[llvm] [WholeProgramDevirt] Add check for AvailableExternal and give up icall.branch.funnel (PR #143468)

Tianle Liu via llvm-commits llvm-commits at lists.llvm.org
Wed Jun 18 23:24:58 PDT 2025


https://github.com/tianleliu updated https://github.com/llvm/llvm-project/pull/143468

>From 49b51cf548222550f15b0e8fa63ec56fdfe95642 Mon Sep 17 00:00:00 2001
From: tianleli <tianle.l.liu at intel.com>
Date: Mon, 9 Jun 2025 15:51:23 +0800
Subject: [PATCH 1/5] [WholeProgramDevirt] Add checking for AvailableExternal
 and gives up devirtualization.

When a customer class inherits from a libc++ class, and is built with
"-flto  -fwhole-program-vtables -static-libstdc++ \
 -Wl,-plugin-opt=-whole-program-visibility", the libc++ class's vtable
is available_externally, meanwhile the customer class vtable is private.
And both of them are !vcall_visibility == Linkage Unit.
In this case, icall.branch.funnel might be generated.

But the icall.branch.funnel would cause crash in LowerTypeTests because
available_externally Global_Object is skipped to save and
leads to a NULL GlobalTypeMember.
Even walking around the crash in LowerTypeTests, it still crashes in
SelectionDAGBuilder or VerifierPass, because they ask operands of
icall.branch.funnel must be the same GlobalValue.

This patch only fix fullLTO mode.
---
 .../lib/Transforms/IPO/WholeProgramDevirt.cpp | 11 ++++
 .../availableexternal-check.ll                | 57 +++++++++++++++++++
 2 files changed, 68 insertions(+)
 create mode 100644 llvm/test/Transforms/WholeProgramDevirt/availableexternal-check.ll

diff --git a/llvm/lib/Transforms/IPO/WholeProgramDevirt.cpp b/llvm/lib/Transforms/IPO/WholeProgramDevirt.cpp
index a7d9f3ba24b24..c0ddb50ea83ed 100644
--- a/llvm/lib/Transforms/IPO/WholeProgramDevirt.cpp
+++ b/llvm/lib/Transforms/IPO/WholeProgramDevirt.cpp
@@ -1093,6 +1093,7 @@ bool DevirtModule::tryFindVirtualCallTargets(
     std::vector<VirtualCallTarget> &TargetsForSlot,
     const std::set<TypeMemberInfo> &TypeMemberInfos, uint64_t ByteOffset,
     ModuleSummaryIndex *ExportSummary) {
+  bool hasAvailableExternally = false;
   for (const TypeMemberInfo &TM : TypeMemberInfos) {
     if (!TM.Bits->GV->isConstant())
       return false;
@@ -1103,6 +1104,16 @@ bool DevirtModule::tryFindVirtualCallTargets(
         GlobalObject::VCallVisibilityPublic)
       return false;
 
+    // Record if the first GV is AvailableExternally
+    if (TargetsForSlot.empty())
+      hasAvailableExternally = TM.Bits->GV->hasAvailableExternallyLinkage();
+
+    // When the first GV is AvailableExternally, check if all other GVs are
+    // also AvailableExternally. If they are not the same, return false.
+    if (!TargetsForSlot.empty() && hasAvailableExternally &&
+        !TM.Bits->GV->hasAvailableExternallyLinkage())
+      return false;
+
     Function *Fn = nullptr;
     Constant *C = nullptr;
     std::tie(Fn, C) =
diff --git a/llvm/test/Transforms/WholeProgramDevirt/availableexternal-check.ll b/llvm/test/Transforms/WholeProgramDevirt/availableexternal-check.ll
new file mode 100644
index 0000000000000..d72075cdc1bb3
--- /dev/null
+++ b/llvm/test/Transforms/WholeProgramDevirt/availableexternal-check.ll
@@ -0,0 +1,57 @@
+; RUN: opt -S -passes=wholeprogramdevirt -whole-program-visibility %s | FileCheck %s
+
+; This test is reduced from C++ code like this:
+; class A :public std::exception {
+; public:
+;   A() {};
+;   const char* what () const throw () {return "A";}
+; };
+; long test(std::exception *p) {
+;   const char* ch = p->what();
+;   return std::strlen(ch);
+; }
+;
+; Build command is "clang++ -O2 -target x86_64-unknown-linux -flto=thin \
+; -fwhole-program-vtables -static-libstdc++  -Wl,-plugin-opt=-whole-program-visibility"
+;
+; _ZTVSt9exception's visibility is 1 (Linkage Unit), and available_externally.
+; But another vtable _ZTV1A.0 is not available_externally.
+; They should not do devirtualization because they are in different linkage type.
+
+target datalayout = "e-m:e-p270:32:32-p271:32:32-p272:64:64-i64:64-i128:128-f80:128-n8:16:32:64-S128"
+target triple = "x86_64-unknown-linux"
+
+ at _ZTVSt9exception = available_externally constant { [5 x ptr] } { [5 x ptr] [ptr null, ptr null, ptr null, ptr null, ptr @_ZNKSt9exception4whatEv] }, !type !0, !type !1, !vcall_visibility !2
+ at _ZTV1A.0 = constant [5 x ptr] [ptr null, ptr null, ptr null, ptr null, ptr @_ZNK1A4whatEv], !type !3, !type !4, !type !5, !type !6, !vcall_visibility !2
+
+declare ptr @_ZNKSt9exception4whatEv()
+
+define i64 @_Z4testPSt9exception() {
+  %1 = call i1 @llvm.type.test(ptr null, metadata !"_ZTSSt9exception")
+  tail call void @llvm.assume(i1 %1)
+  %2 = getelementptr i8, ptr null, i64 16
+  %3 = load ptr, ptr %2, align 8
+  %4 = tail call ptr %3(ptr null)
+  ret i64 0
+}
+
+; Function Attrs: nocallback nofree nosync nounwind willreturn memory(inaccessiblemem: write)
+declare void @llvm.assume(i1 noundef) #0
+
+declare ptr @_ZNK1A4whatEv()
+
+; Function Attrs: nocallback nofree nosync nounwind speculatable willreturn memory(none)
+declare i1 @llvm.type.test(ptr, metadata) #1
+
+; CHECK-NOT: call void (...) @llvm.icall.branch.funnel
+
+attributes #0 = { nocallback nofree nosync nounwind willreturn memory(inaccessiblemem: write) }
+attributes #1 = { nocallback nofree nosync nounwind speculatable willreturn memory(none) }
+
+!0 = !{i64 16, !"_ZTSSt9exception"}
+!1 = !{i64 32, !"_ZTSMSt9exceptionKDoFPKcvE.virtual"}
+!2 = !{i64 1}
+!3 = !{i32 16, !"_ZTS1A"}
+!4 = !{i32 32, !"_ZTSM1AKDoFPKcvE.virtual"}
+!5 = !{i32 16, !"_ZTSSt9exception"}
+!6 = !{i32 32, !"_ZTSMSt9exceptionKDoFPKcvE.virtual"}

>From 0725ff3a5086998dd6d103f928707dbf1ff21648 Mon Sep 17 00:00:00 2001
From: tianleli <tianle.l.liu at intel.com>
Date: Wed, 11 Jun 2025 10:34:26 +0800
Subject: [PATCH 2/5] Fix case and simplify test file.

---
 llvm/lib/Transforms/IPO/WholeProgramDevirt.cpp             | 6 +++---
 .../WholeProgramDevirt/availableexternal-check.ll          | 7 +++----
 2 files changed, 6 insertions(+), 7 deletions(-)

diff --git a/llvm/lib/Transforms/IPO/WholeProgramDevirt.cpp b/llvm/lib/Transforms/IPO/WholeProgramDevirt.cpp
index c0ddb50ea83ed..b86d64756998e 100644
--- a/llvm/lib/Transforms/IPO/WholeProgramDevirt.cpp
+++ b/llvm/lib/Transforms/IPO/WholeProgramDevirt.cpp
@@ -1093,7 +1093,7 @@ bool DevirtModule::tryFindVirtualCallTargets(
     std::vector<VirtualCallTarget> &TargetsForSlot,
     const std::set<TypeMemberInfo> &TypeMemberInfos, uint64_t ByteOffset,
     ModuleSummaryIndex *ExportSummary) {
-  bool hasAvailableExternally = false;
+  bool HasAvailableExternally = false;
   for (const TypeMemberInfo &TM : TypeMemberInfos) {
     if (!TM.Bits->GV->isConstant())
       return false;
@@ -1106,11 +1106,11 @@ bool DevirtModule::tryFindVirtualCallTargets(
 
     // Record if the first GV is AvailableExternally
     if (TargetsForSlot.empty())
-      hasAvailableExternally = TM.Bits->GV->hasAvailableExternallyLinkage();
+      HasAvailableExternally = TM.Bits->GV->hasAvailableExternallyLinkage();
 
     // When the first GV is AvailableExternally, check if all other GVs are
     // also AvailableExternally. If they are not the same, return false.
-    if (!TargetsForSlot.empty() && hasAvailableExternally &&
+    if (!TargetsForSlot.empty() && HasAvailableExternally &&
         !TM.Bits->GV->hasAvailableExternallyLinkage())
       return false;
 
diff --git a/llvm/test/Transforms/WholeProgramDevirt/availableexternal-check.ll b/llvm/test/Transforms/WholeProgramDevirt/availableexternal-check.ll
index d72075cdc1bb3..65d594529ee06 100644
--- a/llvm/test/Transforms/WholeProgramDevirt/availableexternal-check.ll
+++ b/llvm/test/Transforms/WholeProgramDevirt/availableexternal-check.ll
@@ -8,7 +8,7 @@
 ; };
 ; long test(std::exception *p) {
 ;   const char* ch = p->what();
-;   return std::strlen(ch);
+;   ...;
 ; }
 ;
 ; Build command is "clang++ -O2 -target x86_64-unknown-linux -flto=thin \
@@ -21,8 +21,8 @@
 target datalayout = "e-m:e-p270:32:32-p271:32:32-p272:64:64-i64:64-i128:128-f80:128-n8:16:32:64-S128"
 target triple = "x86_64-unknown-linux"
 
- at _ZTVSt9exception = available_externally constant { [5 x ptr] } { [5 x ptr] [ptr null, ptr null, ptr null, ptr null, ptr @_ZNKSt9exception4whatEv] }, !type !0, !type !1, !vcall_visibility !2
- at _ZTV1A.0 = constant [5 x ptr] [ptr null, ptr null, ptr null, ptr null, ptr @_ZNK1A4whatEv], !type !3, !type !4, !type !5, !type !6, !vcall_visibility !2
+ at _ZTVSt9exception = available_externally constant { [5 x ptr] } { [5 x ptr] [ptr null, ptr null, ptr null, ptr null, ptr @_ZNKSt9exception4whatEv] }, !type !0, !type !1
+ at _ZTV1A.0 = constant [5 x ptr] [ptr null, ptr null, ptr null, ptr null, ptr @_ZNK1A4whatEv], !type !3, !type !4, !type !5, !type !6
 
 declare ptr @_ZNKSt9exception4whatEv()
 
@@ -50,7 +50,6 @@ attributes #1 = { nocallback nofree nosync nounwind speculatable willreturn memo
 
 !0 = !{i64 16, !"_ZTSSt9exception"}
 !1 = !{i64 32, !"_ZTSMSt9exceptionKDoFPKcvE.virtual"}
-!2 = !{i64 1}
 !3 = !{i32 16, !"_ZTS1A"}
 !4 = !{i32 32, !"_ZTSM1AKDoFPKcvE.virtual"}
 !5 = !{i32 16, !"_ZTSSt9exception"}

>From fd5e7a19639d362090c608c0372ad7069d0d69fb Mon Sep 17 00:00:00 2001
From: tianleli <tianle.l.liu at intel.com>
Date: Wed, 11 Jun 2025 16:53:10 +0800
Subject: [PATCH 3/5] Fix issue in test file.

---
 .../WholeProgramDevirt/availableexternal-check.ll | 15 ++++++++-------
 1 file changed, 8 insertions(+), 7 deletions(-)

diff --git a/llvm/test/Transforms/WholeProgramDevirt/availableexternal-check.ll b/llvm/test/Transforms/WholeProgramDevirt/availableexternal-check.ll
index 65d594529ee06..5651f9ef86670 100644
--- a/llvm/test/Transforms/WholeProgramDevirt/availableexternal-check.ll
+++ b/llvm/test/Transforms/WholeProgramDevirt/availableexternal-check.ll
@@ -26,13 +26,14 @@ target triple = "x86_64-unknown-linux"
 
 declare ptr @_ZNKSt9exception4whatEv()
 
-define i64 @_Z4testPSt9exception() {
-  %1 = call i1 @llvm.type.test(ptr null, metadata !"_ZTSSt9exception")
-  tail call void @llvm.assume(i1 %1)
-  %2 = getelementptr i8, ptr null, i64 16
-  %3 = load ptr, ptr %2, align 8
-  %4 = tail call ptr %3(ptr null)
-  ret i64 0
+define ptr @_Z4testPSt9exception() {
+  %1 = load ptr, ptr null, align 8
+  %2 = call i1 @llvm.type.test(ptr %1, metadata !"_ZTSSt9exception")
+  tail call void @llvm.assume(i1 %2)
+  %3 = getelementptr i8, ptr %1, i64 16
+  %4 = load ptr, ptr %3, align 8
+  %5 = tail call ptr %4(ptr null)
+  ret ptr %5
 }
 
 ; Function Attrs: nocallback nofree nosync nounwind willreturn memory(inaccessiblemem: write)

>From 14ee5ff45d71528f023a22a1c075f61a7302d95c Mon Sep 17 00:00:00 2001
From: tianleli <tianle.l.liu at intel.com>
Date: Tue, 17 Jun 2025 10:29:38 +0800
Subject: [PATCH 4/5] Postpone check in tryICallBranchFunnel and tighten
 condition to any AE.

---
 llvm/lib/Transforms/IPO/WholeProgramDevirt.cpp  | 17 ++++++-----------
 .../availableexternal-check.ll                  |  5 ++---
 2 files changed, 8 insertions(+), 14 deletions(-)

diff --git a/llvm/lib/Transforms/IPO/WholeProgramDevirt.cpp b/llvm/lib/Transforms/IPO/WholeProgramDevirt.cpp
index b86d64756998e..54c55b2b29901 100644
--- a/llvm/lib/Transforms/IPO/WholeProgramDevirt.cpp
+++ b/llvm/lib/Transforms/IPO/WholeProgramDevirt.cpp
@@ -1093,7 +1093,6 @@ bool DevirtModule::tryFindVirtualCallTargets(
     std::vector<VirtualCallTarget> &TargetsForSlot,
     const std::set<TypeMemberInfo> &TypeMemberInfos, uint64_t ByteOffset,
     ModuleSummaryIndex *ExportSummary) {
-  bool HasAvailableExternally = false;
   for (const TypeMemberInfo &TM : TypeMemberInfos) {
     if (!TM.Bits->GV->isConstant())
       return false;
@@ -1104,16 +1103,6 @@ bool DevirtModule::tryFindVirtualCallTargets(
         GlobalObject::VCallVisibilityPublic)
       return false;
 
-    // Record if the first GV is AvailableExternally
-    if (TargetsForSlot.empty())
-      HasAvailableExternally = TM.Bits->GV->hasAvailableExternallyLinkage();
-
-    // When the first GV is AvailableExternally, check if all other GVs are
-    // also AvailableExternally. If they are not the same, return false.
-    if (!TargetsForSlot.empty() && HasAvailableExternally &&
-        !TM.Bits->GV->hasAvailableExternallyLinkage())
-      return false;
-
     Function *Fn = nullptr;
     Constant *C = nullptr;
     std::tie(Fn, C) =
@@ -1467,6 +1456,12 @@ void DevirtModule::tryICallBranchFunnel(
   if (!HasNonDevirt)
     return;
 
+  // If any GV is AvailableExternally, drop to generate branch.funnel
+  for (auto &T : TargetsForSlot) {
+    if (T.TM->Bits->GV->hasAvailableExternallyLinkage())
+      return;
+  }
+
   FunctionType *FT =
       FunctionType::get(Type::getVoidTy(M.getContext()), {Int8PtrTy}, true);
   Function *JT;
diff --git a/llvm/test/Transforms/WholeProgramDevirt/availableexternal-check.ll b/llvm/test/Transforms/WholeProgramDevirt/availableexternal-check.ll
index 5651f9ef86670..41809b41d8c70 100644
--- a/llvm/test/Transforms/WholeProgramDevirt/availableexternal-check.ll
+++ b/llvm/test/Transforms/WholeProgramDevirt/availableexternal-check.ll
@@ -11,12 +11,11 @@
 ;   ...;
 ; }
 ;
-; Build command is "clang++ -O2 -target x86_64-unknown-linux -flto=thin \
+; Build command is "clang++ -O2 -target x86_64-unknown-linux -flto=full \
 ; -fwhole-program-vtables -static-libstdc++  -Wl,-plugin-opt=-whole-program-visibility"
 ;
 ; _ZTVSt9exception's visibility is 1 (Linkage Unit), and available_externally.
-; But another vtable _ZTV1A.0 is not available_externally.
-; They should not do devirtualization because they are in different linkage type.
+; If any GV is available_externally, icall.branch.funnel should not be generated.
 
 target datalayout = "e-m:e-p270:32:32-p271:32:32-p272:64:64-i64:64-i128:128-f80:128-n8:16:32:64-S128"
 target triple = "x86_64-unknown-linux"

>From a133a2190169198871b795760f8b2ec18720ee20 Mon Sep 17 00:00:00 2001
From: tianleli <tianle.l.liu at intel.com>
Date: Thu, 19 Jun 2025 13:41:43 +0800
Subject: [PATCH 5/5] Add detailed comment.

---
 llvm/lib/Transforms/IPO/WholeProgramDevirt.cpp | 12 +++++++++++-
 1 file changed, 11 insertions(+), 1 deletion(-)

diff --git a/llvm/lib/Transforms/IPO/WholeProgramDevirt.cpp b/llvm/lib/Transforms/IPO/WholeProgramDevirt.cpp
index 54c55b2b29901..642c8e855fbab 100644
--- a/llvm/lib/Transforms/IPO/WholeProgramDevirt.cpp
+++ b/llvm/lib/Transforms/IPO/WholeProgramDevirt.cpp
@@ -1456,7 +1456,17 @@ void DevirtModule::tryICallBranchFunnel(
   if (!HasNonDevirt)
     return;
 
-  // If any GV is AvailableExternally, drop to generate branch.funnel
+  // If any GV is AvailableExternally, not to generate branch.funnel.
+  // NOTE: It is to avoid crash in LowerTypeTest.
+  // If the branch.funnel is generated, because GV.isDeclarationForLinker(),
+  // in LowerTypeTestsModule::lower(), its GlobalTypeMember would NOT
+  // be saved in GlobalTypeMembers[&GV]. Then crash happens in
+  // buildBitSetsFromDisjointSet due to GlobalTypeMembers[&GV] is NULL.
+  // Even doing experiment to save it in GlobalTypeMembers[&GV] and
+  // making GlobalTypeMembers[&GV] be not NULL, crash could avoid from
+  // buildBitSetsFromDisjointSet. But still report_fatal_error in Verifier
+  // or SelectionDAGBuilder later, because operands linkage type consistency
+  // check of icall.branch.funnel can not pass.
   for (auto &T : TargetsForSlot) {
     if (T.TM->Bits->GV->hasAvailableExternallyLinkage())
       return;



More information about the llvm-commits mailing list