[llvm] [MergeFunctions] Preserve import GUIDs when folding functions (PR #207003)

Alok Kumar Sharma via llvm-commits llvm-commits at lists.llvm.org
Thu Jul 30 08:30:11 PDT 2026


https://github.com/alokkrsharma updated https://github.com/llvm/llvm-project/pull/207003

>From 58c117a6333f51cac36b8505d5fa2eb709275d2f Mon Sep 17 00:00:00 2001
From: Alok Kumar Sharma <AlokKumar.Sharma at amd.com>
Date: Wed, 1 Jul 2026 12:46:38 +0530
Subject: [PATCH 1/3] [MergeFunctions] Preserve import GUIDs when folding
 functions

Computes the union of SamplePGO/ThinLTO import GUIDs from both
inputs and attaches it to the surviving function.
---
 llvm/lib/Transforms/IPO/MergeFunctions.cpp    | 33 +++++--
 .../merge-functions-entry-count-imports.ll    | 85 +++++++++++++++++++
 2 files changed, 110 insertions(+), 8 deletions(-)
 create mode 100644 llvm/test/Transforms/MergeFunc/merge-functions-entry-count-imports.ll

diff --git a/llvm/lib/Transforms/IPO/MergeFunctions.cpp b/llvm/lib/Transforms/IPO/MergeFunctions.cpp
index c4e56855ea2fb..3b98dadc68451 100644
--- a/llvm/lib/Transforms/IPO/MergeFunctions.cpp
+++ b/llvm/lib/Transforms/IPO/MergeFunctions.cpp
@@ -90,6 +90,7 @@
 
 #include "llvm/Transforms/IPO/MergeFunctions.h"
 #include "llvm/ADT/ArrayRef.h"
+#include "llvm/ADT/DenseSet.h"
 #include "llvm/ADT/SmallVector.h"
 #include "llvm/ADT/Statistic.h"
 #include "llvm/IR/Argument.h"
@@ -880,12 +881,27 @@ static bool isODR(const Function *F) {
   return F->hasWeakODRLinkage() || F->hasLinkOnceODRLinkage();
 }
 
-static void mergeEntryCountsInto(Function *F, std::optional<uint64_t> FC,
-                                 std::optional<uint64_t> GC) {
-  if (!FC && !GC)
+static DenseSet<GlobalValue::GUID> unionImportGUIDs(const Function *F,
+                                                    const Function *G) {
+  DenseSet<GlobalValue::GUID> AllImports = F->getImportGUIDs();
+  DenseSet<GlobalValue::GUID> GImports = G->getImportGUIDs();
+  AllImports.insert(GImports.begin(), GImports.end());
+  return AllImports;
+}
+
+static void
+mergeEntryCountsAndImportsInto(Function *F, std::optional<uint64_t> FC,
+                               std::optional<uint64_t> GC,
+                               const DenseSet<GlobalValue::GUID> &Imports) {
+  if (!FC && !GC && Imports.empty())
     return;
-  uint64_t Sum = SaturatingAdd(FC ? *FC : uint64_t{0}, GC ? *GC : uint64_t{0});
-  F->setEntryCount(Sum);
+
+  uint64_t Sum;
+  if (!FC && !GC)
+    Sum = static_cast<uint64_t>(-1);
+  else
+    Sum = SaturatingAdd(FC ? *FC : uint64_t{0}, GC ? *GC : uint64_t{0});
+  F->setEntryCount(Sum, Imports.empty() ? nullptr : &Imports);
 }
 
 // Merge two equivalent functions. Upon completion, Function G is deleted.
@@ -893,6 +909,7 @@ void MergeFunctions::mergeTwoFunctions(Function *F, Function *G) {
 
   std::optional<uint64_t> FEC = F->getEntryCount();
   std::optional<uint64_t> GEC = G->getEntryCount();
+  DenseSet<GlobalValue::GUID> AllImports = unionImportGUIDs(F, G);
 
   // Create a new thunk that both F and G can call, if F cannot call G directly.
   // That is the case if F is either interposable or if G is either weak_odr or
@@ -947,7 +964,7 @@ void MergeFunctions::mergeTwoFunctions(Function *F, Function *G) {
     F->setLinkage(GlobalValue::PrivateLinkage);
     // The private shared implementation accumulates both symbols' entries
     // (FEC + GEC), while each ODR thunk retains its own per-symbol entry count.
-    mergeEntryCountsInto(F, FEC, GEC);
+    mergeEntryCountsAndImportsInto(F, FEC, GEC, AllImports);
     ++NumDoubleWeak;
     ++NumFunctionsMerged;
   } else {
@@ -975,14 +992,14 @@ void MergeFunctions::mergeTwoFunctions(Function *F, Function *G) {
     // stop here and delete G. There's no need for a thunk. (See note on
     // MergeFunctionsPDI above).
     if (G->isDiscardableIfUnused() && G->use_empty() && !MergeFunctionsPDI) {
-      mergeEntryCountsInto(F, FEC, GEC);
+      mergeEntryCountsAndImportsInto(F, FEC, GEC, AllImports);
       G->eraseFromParent();
       ++NumFunctionsMerged;
       return;
     }
 
     if (writeThunkOrAliasIfNeeded(F, G)) {
-      mergeEntryCountsInto(F, FEC, GEC);
+      mergeEntryCountsAndImportsInto(F, FEC, GEC, AllImports);
       ++NumFunctionsMerged;
     }
   }
diff --git a/llvm/test/Transforms/MergeFunc/merge-functions-entry-count-imports.ll b/llvm/test/Transforms/MergeFunc/merge-functions-entry-count-imports.ll
new file mode 100644
index 0000000000000..a3d972f7e984a
--- /dev/null
+++ b/llvm/test/Transforms/MergeFunc/merge-functions-entry-count-imports.ll
@@ -0,0 +1,85 @@
+; NOTE: Assertions have been autogenerated by utils/update_test_checks.py UTC_ARGS: --version 6
+; RUN: opt < %s -passes=mergefunc -S | FileCheck %s
+
+define internal i32 @foo(i32 %x) !prof !12 {
+entry:
+  %add = add nsw i32 %x, 1
+  ret i32 %add
+}
+
+define internal i32 @bar(i32 %x) !prof !13 {
+; CHECK-LABEL: define internal i32 @bar(
+; CHECK-SAME: i32 [[X:%.*]]) !prof [[PROF12:![0-9]+]] {
+; CHECK-NEXT:  [[ENTRY:.*:]]
+; CHECK-NEXT:    [[ADD:%.*]] = add nsw i32 [[X]], 1
+; CHECK-NEXT:    ret i32 [[ADD]]
+;
+entry:
+  %add = add nsw i32 %x, 1
+  ret i32 %add
+}
+
+define internal i32 @f(i32 %x) !prof !14 {
+; CHECK-LABEL: define internal i32 @f(
+; CHECK-SAME: i32 [[X:%.*]]) !prof [[PROF13:![0-9]+]] {
+; CHECK-NEXT:  [[ENTRY:.*:]]
+; CHECK-NEXT:    [[ADD:%.*]] = add nsw i32 [[X]], 2
+; CHECK-NEXT:    ret i32 [[ADD]]
+;
+entry:
+  %add = add nsw i32 %x, 2
+  ret i32 %add
+}
+
+define internal i32 @g(i32 %x) !prof !15 {
+entry:
+  %add = add nsw i32 %x, 2
+  ret i32 %add
+}
+
+define i32 @main() {
+; CHECK-LABEL: define i32 @main() {
+; CHECK-NEXT:  [[ENTRY:.*:]]
+; CHECK-NEXT:    [[C1:%.*]] = call i32 @bar(i32 1)
+; CHECK-NEXT:    [[C2:%.*]] = call i32 @bar(i32 2)
+; CHECK-NEXT:    [[C3:%.*]] = call i32 @f(i32 3)
+; CHECK-NEXT:    [[C4:%.*]] = call i32 @f(i32 4)
+; CHECK-NEXT:    [[R1:%.*]] = add nsw i32 [[C1]], [[C2]]
+; CHECK-NEXT:    [[R2:%.*]] = add nsw i32 [[C3]], [[C4]]
+; CHECK-NEXT:    [[R:%.*]] = add nsw i32 [[R1]], [[R2]]
+; CHECK-NEXT:    ret i32 [[R]]
+;
+entry:
+  %c1 = call i32 @foo(i32 1)
+  %c2 = call i32 @bar(i32 2)
+  %c3 = call i32 @f(i32 3)
+  %c4 = call i32 @g(i32 4)
+  %r1 = add nsw i32 %c1, %c2
+  %r2 = add nsw i32 %c3, %c4
+  %r = add nsw i32 %r1, %r2
+  ret i32 %r
+}
+
+!llvm.module.flags = !{!0}
+
+!0 = !{i32 1, !"ProfileSummary", !1}
+!1 = !{!2, !3, !4, !5, !6, !7, !8, !9}
+!2 = !{!"ProfileFormat", !"InstrProf"}
+!3 = !{!"TotalCount", i64 300}
+!4 = !{!"MaxCount", i64 200}
+!5 = !{!"MaxInternalCount", i64 200}
+!6 = !{!"MaxFunctionCount", i64 200}
+!7 = !{!"NumCounts", i64 2}
+!8 = !{!"NumFunctions", i64 2}
+!9 = !{!"DetailedSummary", !10}
+!10 = !{!11}
+!11 = !{i32 10000, i64 100, i32 1}
+!12 = !{!"function_entry_count", i64 100, i64 1001, i64 2002}
+!13 = !{!"function_entry_count", i64 200, i64 2002, i64 3003}
+!14 = !{!"function_entry_count", i64 -1, i64 1001, i64 2002}
+!15 = !{!"function_entry_count", i64 -1, i64 2002, i64 3003}
+
+;.
+; CHECK: [[PROF12]] = !{!"function_entry_count", i64 300, i64 1001, i64 2002, i64 3003}
+; CHECK: [[PROF13]] = !{!"function_entry_count", i64 -1, i64 1001, i64 2002, i64 3003}
+;.

>From 0ecf5724a1a515b6cb9f1e5ea2f3cc5a41b278f5 Mon Sep 17 00:00:00 2001
From: Alok Kumar Sharma <AlokKumar.Sharma at amd.com>
Date: Mon, 27 Jul 2026 21:12:30 +0530
Subject: [PATCH 2/3] Review comments.

---
 llvm/lib/Transforms/IPO/MergeFunctions.cpp | 103 ++++++++++++---------
 1 file changed, 60 insertions(+), 43 deletions(-)

diff --git a/llvm/lib/Transforms/IPO/MergeFunctions.cpp b/llvm/lib/Transforms/IPO/MergeFunctions.cpp
index 3b98dadc68451..9cb950b26288b 100644
--- a/llvm/lib/Transforms/IPO/MergeFunctions.cpp
+++ b/llvm/lib/Transforms/IPO/MergeFunctions.cpp
@@ -288,7 +288,9 @@ class MergeFunctions {
   // If needed, replace G with an alias to F if possible, or a thunk to F if
   // profitable. Returns false if neither is the case. If \p G is not needed
   // (i.e. it is discardable and not used), \p G is removed directly.
-  bool writeThunkOrAliasIfNeeded(Function *F, Function *G);
+  // \p MergeProfile must be true when G's profile should be preserved, it is
+  // merged into F before G is erased or rewritten.
+  bool writeThunkOrAliasIfNeeded(Function *F, Function *G, bool MergeProfile);
 
   /// Replace function F with function G in the function tree.
   void replaceFunctionInTree(const FunctionNode &FN, Function *G);
@@ -857,23 +859,64 @@ void MergeFunctions::writeAlias(Function *F, Function *G) {
   ++NumAliasesWritten;
 }
 
+static DenseSet<GlobalValue::GUID> unionImportGUIDs(const Function &F,
+                                                    const Function &G) {
+  DenseSet<GlobalValue::GUID> AllImports = F.getImportGUIDs();
+  DenseSet<GlobalValue::GUID> GImports = G.getImportGUIDs();
+  AllImports.insert(GImports.begin(), GImports.end());
+  return AllImports;
+}
+
+static void mergeEntryCountsAndImportsInto(Function &F, Function &G) {
+  std::optional<uint64_t> FEntryCount = F.getEntryCount();
+  std::optional<uint64_t> GEntryCount = G.getEntryCount();
+  DenseSet<GlobalValue::GUID> AllImports = unionImportGUIDs(F, G);
+  if (!FEntryCount && !GEntryCount && AllImports.empty())
+    return;
+
+  // -1 is a safe placeholder here, getEntryCount() already treats it as
+  // "unknown" (same sentinel SamplePGO uses for no-sample functions), so
+  // it won't look hot to anyone reading the count back.
+  uint64_t Sum = static_cast<uint64_t>(-1);
+  if (FEntryCount || GEntryCount)
+    Sum = SaturatingAdd(FEntryCount ? *FEntryCount : uint64_t{0},
+                        GEntryCount ? *GEntryCount : uint64_t{0});
+  F.setEntryCount(Sum, AllImports.empty() ? nullptr : &AllImports);
+}
+
 // If needed, replace G with an alias to F if possible, or a thunk to F if
 // profitable. Returns false if neither is the case. If \p G is not needed (i.e.
-// it is discardable and unused), \p G is removed directly.
-bool MergeFunctions::writeThunkOrAliasIfNeeded(Function *F, Function *G) {
-  if (G->isDiscardableIfUnused() && G->use_empty() && !MergeFunctionsPDI) {
+// it is discardable and unused), \p G is removed directly. If \p MergeProfile
+// is set, G's profile metadata is merged into F.
+bool MergeFunctions::writeThunkOrAliasIfNeeded(Function *F, Function *G,
+                                               bool MergeProfile) {
+  bool ShouldErase =
+      G->isDiscardableIfUnused() && G->use_empty() && !MergeFunctionsPDI;
+  bool ShouldAlias = canCreateAliasFor(G);
+  bool ShouldThunk = canCreateThunkFor(F);
+
+  if (!ShouldErase && !ShouldAlias && !ShouldThunk)
+    return false;
+
+  if (MergeProfile) {
+    mergeEntryCountsAndImportsInto(*F, *G);
+  }
+
+  if (ShouldErase) {
     G->eraseFromParent();
     return true;
   }
-  if (canCreateAliasFor(G)) {
+
+  if (ShouldAlias) {
     writeAlias(F, G);
     return true;
   }
-  if (canCreateThunkFor(F)) {
+  if (ShouldThunk) {
     writeThunk(F, G);
     return true;
   }
-  return false;
+
+  llvm_unreachable("Erase, alias or thunk must apply");
 }
 
 /// Returns true if \p F is either weak_odr or linkonce_odr.
@@ -881,35 +924,10 @@ static bool isODR(const Function *F) {
   return F->hasWeakODRLinkage() || F->hasLinkOnceODRLinkage();
 }
 
-static DenseSet<GlobalValue::GUID> unionImportGUIDs(const Function *F,
-                                                    const Function *G) {
-  DenseSet<GlobalValue::GUID> AllImports = F->getImportGUIDs();
-  DenseSet<GlobalValue::GUID> GImports = G->getImportGUIDs();
-  AllImports.insert(GImports.begin(), GImports.end());
-  return AllImports;
-}
-
-static void
-mergeEntryCountsAndImportsInto(Function *F, std::optional<uint64_t> FC,
-                               std::optional<uint64_t> GC,
-                               const DenseSet<GlobalValue::GUID> &Imports) {
-  if (!FC && !GC && Imports.empty())
-    return;
-
-  uint64_t Sum;
-  if (!FC && !GC)
-    Sum = static_cast<uint64_t>(-1);
-  else
-    Sum = SaturatingAdd(FC ? *FC : uint64_t{0}, GC ? *GC : uint64_t{0});
-  F->setEntryCount(Sum, Imports.empty() ? nullptr : &Imports);
-}
-
 // Merge two equivalent functions. Upon completion, Function G is deleted.
 void MergeFunctions::mergeTwoFunctions(Function *F, Function *G) {
 
-  std::optional<uint64_t> FEC = F->getEntryCount();
-  std::optional<uint64_t> GEC = G->getEntryCount();
-  DenseSet<GlobalValue::GUID> AllImports = unionImportGUIDs(F, G);
+  std::optional<uint64_t> FEntryCount = F->getEntryCount();
 
   // Create a new thunk that both F and G can call, if F cannot call G directly.
   // That is the case if F is either interposable or if G is either weak_odr or
@@ -952,19 +970,19 @@ void MergeFunctions::mergeTwoFunctions(Function *F, Function *G) {
     const MaybeAlign NewFAlign = NewF->getAlign();
     const MaybeAlign GAlign = G->getAlign();
 
-    writeThunkOrAliasIfNeeded(F, G);
-    if (FEC)
-      NewF->setEntryCount(*FEC);
-    writeThunkOrAliasIfNeeded(F, NewF);
+    // Merge !prof, while G still has its body.
+    writeThunkOrAliasIfNeeded(F, G, /*MergeProfile*/ true);
+    if (FEntryCount)
+      NewF->setEntryCount(*FEntryCount);
+    // NewF becomes thunk/alias to the shared body F, it has no profile to be
+    // merged.
+    writeThunkOrAliasIfNeeded(F, NewF, /*MergeProfile*/ false);
 
     if (NewFAlign || GAlign)
       F->setAlignment(std::max(NewFAlign.valueOrOne(), GAlign.valueOrOne()));
     else
       F->setAlignment(std::nullopt);
     F->setLinkage(GlobalValue::PrivateLinkage);
-    // The private shared implementation accumulates both symbols' entries
-    // (FEC + GEC), while each ODR thunk retains its own per-symbol entry count.
-    mergeEntryCountsAndImportsInto(F, FEC, GEC, AllImports);
     ++NumDoubleWeak;
     ++NumFunctionsMerged;
   } else {
@@ -992,14 +1010,13 @@ void MergeFunctions::mergeTwoFunctions(Function *F, Function *G) {
     // stop here and delete G. There's no need for a thunk. (See note on
     // MergeFunctionsPDI above).
     if (G->isDiscardableIfUnused() && G->use_empty() && !MergeFunctionsPDI) {
-      mergeEntryCountsAndImportsInto(F, FEC, GEC, AllImports);
+      mergeEntryCountsAndImportsInto(*F, *G);
       G->eraseFromParent();
       ++NumFunctionsMerged;
       return;
     }
 
-    if (writeThunkOrAliasIfNeeded(F, G)) {
-      mergeEntryCountsAndImportsInto(F, FEC, GEC, AllImports);
+    if (writeThunkOrAliasIfNeeded(F, G, /*MergeProfile*/ true)) {
       ++NumFunctionsMerged;
     }
   }

>From 659bff722c4e38abd6a1e1c70019ff8392110373 Mon Sep 17 00:00:00 2001
From: Alok Kumar Sharma <AlokKumar.Sharma at amd.com>
Date: Thu, 30 Jul 2026 20:26:40 +0530
Subject: [PATCH 3/3] Review comments.

---
 llvm/lib/Transforms/IPO/MergeFunctions.cpp | 6 ++----
 1 file changed, 2 insertions(+), 4 deletions(-)

diff --git a/llvm/lib/Transforms/IPO/MergeFunctions.cpp b/llvm/lib/Transforms/IPO/MergeFunctions.cpp
index 9cb950b26288b..9ea9dbfa3a048 100644
--- a/llvm/lib/Transforms/IPO/MergeFunctions.cpp
+++ b/llvm/lib/Transforms/IPO/MergeFunctions.cpp
@@ -898,9 +898,8 @@ bool MergeFunctions::writeThunkOrAliasIfNeeded(Function *F, Function *G,
   if (!ShouldErase && !ShouldAlias && !ShouldThunk)
     return false;
 
-  if (MergeProfile) {
+  if (MergeProfile)
     mergeEntryCountsAndImportsInto(*F, *G);
-  }
 
   if (ShouldErase) {
     G->eraseFromParent();
@@ -1016,9 +1015,8 @@ void MergeFunctions::mergeTwoFunctions(Function *F, Function *G) {
       return;
     }
 
-    if (writeThunkOrAliasIfNeeded(F, G, /*MergeProfile*/ true)) {
+    if (writeThunkOrAliasIfNeeded(F, G, /*MergeProfile*/ true))
       ++NumFunctionsMerged;
-    }
   }
 }
 



More information about the llvm-commits mailing list