[llvm] [AArch64] Extend -fno-plt to SelectionDAG calls (PR #206659)
via llvm-commits
llvm-commits at lists.llvm.org
Mon Jun 29 23:57:44 PDT 2026
https://github.com/lfeng14 created https://github.com/llvm/llvm-project/pull/206659
This fixes AArch64 SelectionDAG calls not honoring `-fno-plt`.
In the workload reported in #126176, Clang still emitted many PLT calls for
preemptible external function calls even when built with `-fno-plt`. As a
result, hot call paths retained unnecessary PLT indirection, leading to a
measurable performance regression.
With this patch, the workload improves by more than 16%. The number of PLT
entries in `libglobal_api.so`, measured with:
```sh
objdump -d libglobal_api.so | grep "@plt>:" | wc -l
```
drops from 25258 to 2274 after the fix. For comparison, GCC produces only 56
PLT entries for the same workload. Top-down analysis also shows the frontend
bottleneck decreases by about 5%.
The root cause is that AArch64 `classifyGlobalFunctionReference` only selected
GOT-based calls for functions marked `NonLazyBind`. Consequently, regular
external function calls lowered through SelectionDAG could still use PLT calls
even when `-fno-plt` set the `RtLibUseGOT` module flag.
This patch extends the same classification path to honor the `RtLibUseGOT`
module flag while preserving the existing `shouldAssumeDSOLocal` and
local-linkage exclusions. As a result, the default behavior is unchanged, and
calls known to be non-preemptible are not forced through the GOT. GOT-based
calls are selected only when the user explicitly requests `-fno-plt`.
The patch also adds TLI entries for `_Unwind_Resume`,
`__cxa_allocate_exception`, and `__cxa_begin_catch`, and marks
`_Unwind_Resume` as `nonlazybind` when `DwarfEHPrepare` creates it under
`RtLibUseGOT`.
Fixes #126176.
Testing:
* `ninja -C build-fno-plt-min -j1 llc FileCheck llvm-tli-checker AnalysisTests`
* `build-fno-plt-min/bin/llvm-lit -sv llvm/test/CodeGen/AArch64/fno-plt.ll`
* `build-fno-plt-min/unittests/Analysis/AnalysisTests --gtest_filter=TargetLibraryInfoTest.ValidProto`
>From b1bc7d17e66a9c314a81c14ef749a4ccc3613155 Mon Sep 17 00:00:00 2001
From: Liu Zhongying <liuzhongying at huawei.com>
Date: Mon, 29 Jun 2026 17:25:04 +0800
Subject: [PATCH] [AArch64] Extend -fno-plt to SelectionDAG calls
AArch64 currently only routes NonLazyBind function references through the GOT in classifyGlobalFunctionReference. As a result, a normal external call selected by SelectionDAG can still become a PLT call even when Clang was invoked with -fno-plt. This leaves extra PLT indirection in cases where the user explicitly requested GOT-based calls, and differs from GCC behavior reported in llvm/llvm-project#126176.
Honor the RtLibUseGOT module flag in the same classification path, while keeping the existing local/DSO-local exclusions so calls known to be non-preemptible are not forced through the GOT. This makes explicit -fno-plt affect preemptible AArch64 function calls selected by SelectionDAG, complementing the existing RtLibUseGOT handling for ExternalSymbol calls in AArch64 FastISel, GlobalISel, and ISel lowering.
Also add the exception runtime entry points used by the new coverage to TargetLibraryInfo and mark _Unwind_Resume as nonlazybind when DwarfEHPrepare creates it under RtLibUseGOT.
Fixes #126176
Co-authored-by: Chen Zheng <czhengsz at cn.ibm.com>
---
.../llvm/Analysis/TargetLibraryInfo.td | 9 +++
llvm/lib/CodeGen/DwarfEHPrepare.cpp | 6 ++
llvm/lib/Target/AArch64/AArch64Subtarget.cpp | 6 +-
llvm/test/CodeGen/AArch64/fno-plt.ll | 80 +++++++++++++++++++
.../tools/llvm-tli-checker/ps4-tli-check.yaml | 11 ++-
.../Analysis/TargetLibraryInfoTest.cpp | 3 +
6 files changed, 110 insertions(+), 5 deletions(-)
create mode 100644 llvm/test/CodeGen/AArch64/fno-plt.ll
diff --git a/llvm/include/llvm/Analysis/TargetLibraryInfo.td b/llvm/include/llvm/Analysis/TargetLibraryInfo.td
index 6b17e70b87f55..0d4dc2ee84c6b 100644
--- a/llvm/include/llvm/Analysis/TargetLibraryInfo.td
+++ b/llvm/include/llvm/Analysis/TargetLibraryInfo.td
@@ -370,6 +370,15 @@ def cxa_guard_acquire : TargetLibCall<"__cxa_guard_acquire", Int, [Ptr]>;
/// void __cxa_guard_release(guard_t *guard);
def cxa_guard_release : TargetLibCall<"__cxa_guard_release", Void, [Ptr]>;
+/// void *_Unwind_Resume(_Unwind_Exception *);
+def unwind_resume : TargetLibCall<"_Unwind_Resume", Void, [Ptr]>;
+
+/// void *__cxa_allocate_exception(size_t thrown_size);
+def cxa_allocate_exception : TargetLibCall<"__cxa_allocate_exception", Ptr, [SizeT]>;
+
+/// void *__cxa_begin_catch(void *exception);
+def cxa_begin_catch : TargetLibCall<"__cxa_begin_catch", Ptr, [Ptr]>;
+
/// double __exp10_finite(double x);
def exp10_finite : TargetLibCall<"__exp10_finite", Dbl, [Dbl]>;
diff --git a/llvm/lib/CodeGen/DwarfEHPrepare.cpp b/llvm/lib/CodeGen/DwarfEHPrepare.cpp
index a4544a61561e5..467b6341dc507 100644
--- a/llvm/lib/CodeGen/DwarfEHPrepare.cpp
+++ b/llvm/lib/CodeGen/DwarfEHPrepare.cpp
@@ -236,6 +236,12 @@ bool DwarfEHPrepare::InsertUnwindResumeCalls() {
}
RewindFunction = F.getParent()->getOrInsertFunction(RewindName, FTy);
+ // When -fno-plt is enabled, mark _Unwind_Resume as non-lazy-bind to ensure
+ // it goes through GOT instead of PLT.
+ if (F.getParent()->getRtLibUseGOT())
+ if (Function *RewindFn = dyn_cast<Function>(RewindFunction.getCallee()))
+ RewindFn->addFnAttr(Attribute::NonLazyBind);
+
// Create the basic block where the _Unwind_Resume call will live.
if (ResumesLeft == 1) {
// Instead of creating a new BB and PHI node, just append the call to
diff --git a/llvm/lib/Target/AArch64/AArch64Subtarget.cpp b/llvm/lib/Target/AArch64/AArch64Subtarget.cpp
index 9ee3d0cc9e0ea..2e60d4d5d4256 100644
--- a/llvm/lib/Target/AArch64/AArch64Subtarget.cpp
+++ b/llvm/lib/Target/AArch64/AArch64Subtarget.cpp
@@ -483,9 +483,13 @@ unsigned AArch64Subtarget::classifyGlobalFunctionReference(
return AArch64II::MO_GOT;
// NonLazyBind goes via GOT unless we know it's available locally.
+ // When -fno-plt is enabled (getRtLibUseGOT), all external function calls
+ // go through GOT, matching the X86 backend behavior.
auto *F = dyn_cast<Function>(GV);
if ((!isTargetMachO() || MachOUseNonLazyBind) && F &&
- F->hasFnAttribute(Attribute::NonLazyBind) && !TM.shouldAssumeDSOLocal(GV))
+ (F->hasFnAttribute(Attribute::NonLazyBind) ||
+ F->getParent()->getRtLibUseGOT()) &&
+ !(TM.shouldAssumeDSOLocal(GV) || GV->hasLocalLinkage()))
return AArch64II::MO_GOT;
if (getTargetTriple().isOSWindows()) {
diff --git a/llvm/test/CodeGen/AArch64/fno-plt.ll b/llvm/test/CodeGen/AArch64/fno-plt.ll
new file mode 100644
index 0000000000000..aa2040fc55736
--- /dev/null
+++ b/llvm/test/CodeGen/AArch64/fno-plt.ll
@@ -0,0 +1,80 @@
+; RUN: llc -mtriple=aarch64-linux-gnu -relocation-model=pic %s -o - | FileCheck %s
+; RUN: llc -mtriple=aarch64-linux-gnu -relocation-model=pic %s -o - -fast-isel | FileCheck %s
+
+; Test that -fno-plt (RtLibUseGOT module flag) routes all external function
+; calls through GOT instead of PLT. This covers regular external functions,
+; functions with NonLazyBind attribute, and C++ exception handling library
+; functions (__cxa_allocate_exception, __cxa_begin_catch, _Unwind_Resume).
+
+declare void @external_func()
+
+define void @caller_external() nounwind {
+; CHECK-LABEL: caller_external:
+; CHECK: adrp x8, :got:external_func
+; CHECK: ldr x8, [x8, :got_lo12:external_func]
+; CHECK: blr x8
+ call void @external_func()
+ ret void
+}
+
+; Function with NonLazyBind attribute — should also use GOT.
+declare void @external_func_nonlazy() #0
+
+define void @caller_nonlazy() nounwind {
+; CHECK-LABEL: caller_nonlazy:
+; CHECK: adrp x8, :got:external_func_nonlazy
+; CHECK: ldr x8, [x8, :got_lo12:external_func_nonlazy]
+; CHECK: blr x8
+ call void @external_func_nonlazy()
+ ret void
+}
+
+; __cxa_allocate_exception — registered in RuntimeLibcalls.td, should use GOT.
+declare ptr @__cxa_allocate_exception(i64)
+
+define void @caller_throw() nounwind {
+; CHECK-LABEL: caller_throw:
+; CHECK: adrp [[ADRP:x[0-9]+]], :got:__cxa_allocate_exception
+; CHECK: ldr [[LD:x[0-9]+]], [[[ADRP]], :got_lo12:__cxa_allocate_exception]
+; CHECK: blr [[LD]]
+ %p = call ptr @__cxa_allocate_exception(i64 4)
+ ret void
+}
+
+; __cxa_begin_catch — registered in RuntimeLibcalls.td, should use GOT.
+declare ptr @__cxa_begin_catch(ptr)
+
+define void @caller_catch() nounwind {
+; CHECK-LABEL: caller_catch:
+; CHECK: adrp [[ADRP:x[0-9]+]], :got:__cxa_begin_catch
+; CHECK: ldr [[LD:x[0-9]+]], [[[ADRP]], :got_lo12:__cxa_begin_catch]
+; CHECK: blr [[LD]]
+ %p = call ptr @__cxa_begin_catch(ptr null)
+ ret void
+}
+
+; _Unwind_Resume — inserted by DwarfEHPrepare from a resume instruction.
+; With -fno-plt, DwarfEHPrepare marks it NonLazyBind, and
+; classifyGlobalFunctionReference routes it through GOT.
+declare i32 @__gxx_personality_v0(...)
+declare void @cleanup_func()
+
+define void @caller_resume() personality ptr @__gxx_personality_v0 {
+; CHECK-LABEL: caller_resume:
+; CHECK: adrp [[ADRP:x[0-9]+]], :got:_Unwind_Resume
+; CHECK: ldr [[LD:x[0-9]+]], [[[ADRP]], :got_lo12:_Unwind_Resume]
+; CHECK: blr [[LD]]
+entry:
+ invoke void @external_func() to label %cont unwind label %lpad
+cont:
+ ret void
+lpad:
+ %lpi = landingpad { ptr, i32 } cleanup
+ call void @cleanup_func()
+ resume { ptr, i32 } %lpi
+}
+
+attributes #0 = { nonlazybind }
+
+!llvm.module.flags = !{!0}
+!0 = !{i32 7, !"RtLibUseGOT", i32 1}
diff --git a/llvm/test/tools/llvm-tli-checker/ps4-tli-check.yaml b/llvm/test/tools/llvm-tli-checker/ps4-tli-check.yaml
index 63520a0b98089..4bafd558e6bba 100644
--- a/llvm/test/tools/llvm-tli-checker/ps4-tli-check.yaml
+++ b/llvm/test/tools/llvm-tli-checker/ps4-tli-check.yaml
@@ -32,7 +32,7 @@
# RUN: FileCheck %s --check-prefix=AVAIL --input-file %t3.txt
# RUN: FileCheck %s --check-prefix=UNAVAIL --input-file %t3.txt
#
-# CHECK: << Total TLI yes SDK no: 18
+# CHECK: << Total TLI yes SDK no: 21
# CHECK: >> Total TLI no SDK yes: 0
# CHECK: == Total TLI yes SDK yes: 277
#
@@ -40,13 +40,16 @@
# WRONG_DETAIL: >> TLI no SDK yes: '_ZdaPvj' aka operator delete[](void*, unsigned int)
# WRONG_DETAIL-COUNT-8: << TLI yes SDK no : {{.*}}__hot_cold_t
# WRONG_DETAIL-COUNT-4: << TLI yes SDK no : '__size_returning_new{{.*}}
+# WRONG_DETAIL: << TLI yes SDK no : '_Unwind_Resume'
+# WRONG_DETAIL: << TLI yes SDK no : '__cxa_allocate_exception'
+# WRONG_DETAIL: << TLI yes SDK no : '__cxa_begin_catch'
# WRONG_DETAIL: << TLI yes SDK no : 'fmaximum_num'
# WRONG_DETAIL: << TLI yes SDK no : 'fmaximum_numf'
# WRONG_DETAIL: << TLI yes SDK no : 'fmaximum_numl'
# WRONG_DETAIL: << TLI yes SDK no : 'fminimum_num'
# WRONG_DETAIL: << TLI yes SDK no : 'fminimum_numf'
# WRONG_DETAIL: << TLI yes SDK no : 'fminimum_numl'
-# WRONG_SUMMARY: << Total TLI yes SDK no: 19{{$}}
+# WRONG_SUMMARY: << Total TLI yes SDK no: 22{{$}}
# WRONG_SUMMARY: >> Total TLI no SDK yes: 1{{$}}
# WRONG_SUMMARY: == Total TLI yes SDK yes: 276
#
@@ -54,8 +57,8 @@
## the exact count first; the two directives should add up to that.
## Yes, this means additions to TLI will fail this test, but the argument
## to -COUNT can't be an expression.
-# AVAIL: TLI knows 528 symbols, 295 available
-# AVAIL-COUNT-295: {{^}} available
+# AVAIL: TLI knows 531 symbols, 298 available
+# AVAIL-COUNT-298: {{^}} available
# AVAIL-NOT: {{^}} available
# UNAVAIL-COUNT-233: not available
# UNAVAIL-NOT: not available
diff --git a/llvm/unittests/Analysis/TargetLibraryInfoTest.cpp b/llvm/unittests/Analysis/TargetLibraryInfoTest.cpp
index afeefd36a11e7..df0a402095d23 100644
--- a/llvm/unittests/Analysis/TargetLibraryInfoTest.cpp
+++ b/llvm/unittests/Analysis/TargetLibraryInfoTest.cpp
@@ -560,7 +560,10 @@ TEST_F(TargetLibraryInfoTest, ValidProto) {
"declare void @\"??_V at YAXPAXI@Z\"(ptr, i32)\n"
// These other functions were derived from the .def C declaration.
+ "declare void @_Unwind_Resume(ptr)\n"
+ "declare ptr @__cxa_allocate_exception(i64)\n"
"declare i32 @__cxa_atexit(ptr, ptr, ptr)\n"
+ "declare ptr @__cxa_begin_catch(ptr)\n"
"declare void @__cxa_guard_abort(ptr)\n"
"declare i32 @__cxa_guard_acquire(ptr)\n"
"declare void @__cxa_guard_release(ptr)\n"
More information about the llvm-commits
mailing list