[llvm] [BOLT][AArch64] Add indirect call promotion support (PR #184733)
Haibo Jiang via llvm-commits
llvm-commits at lists.llvm.org
Thu Jun 11 06:09:42 PDT 2026
https://github.com/Jianghibo updated https://github.com/llvm/llvm-project/pull/184733
>From ee4a1c221874e5ecbe9a51294e9359ddbe72c1aa Mon Sep 17 00:00:00 2001
From: jianghaibo <jianghaibo9 at huawei.com>
Date: Thu, 5 Mar 2026 11:32:58 +0800
Subject: [PATCH] [BOLT][AArch64] Support call ICP
Add AArch64 code generation for indirect call promotion. The implementation
scavenges a temporary register for the callee address and emits compare/branch
sequences for promoted direct call and tail call targets.
Keep jump-table ICP disabled on AArch64 until BOLT can reliably recover the
jump table.
---
bolt/include/bolt/Core/MCPlusBuilder.h | 12 +-
bolt/lib/Passes/IndirectCallPromotion.cpp | 35 ++++-
.../Target/AArch64/AArch64MCPlusBuilder.cpp | 147 ++++++++++++++++++
bolt/lib/Target/X86/X86MCPlusBuilder.cpp | 2 +-
bolt/test/AArch64/icp-inline.s | 118 ++++++++++++++
5 files changed, 306 insertions(+), 8 deletions(-)
create mode 100644 bolt/test/AArch64/icp-inline.s
diff --git a/bolt/include/bolt/Core/MCPlusBuilder.h b/bolt/include/bolt/Core/MCPlusBuilder.h
index 76b4a5fe778c0..5ffeb61d21590 100644
--- a/bolt/include/bolt/Core/MCPlusBuilder.h
+++ b/bolt/include/bolt/Core/MCPlusBuilder.h
@@ -2077,6 +2077,16 @@ class MCPlusBuilder {
return {};
}
+ /// Create a sequence of instructions to compare contents of a register
+ /// \p Reg1 to a register \p Reg2 and jump to \p Target if they are different.
+ virtual InstructionListType createCmpJNEWithReg(MCPhysReg Reg1,
+ MCPhysReg Reg2,
+ const MCSymbol *Target,
+ MCContext *Ctx) const {
+ llvm_unreachable("not implemented");
+ return {};
+ }
+
/// Find memcpy size in bytes by using preceding instructions.
/// Returns std::nullopt if size cannot be determined (no-op for most
/// targets).
@@ -2501,7 +2511,7 @@ class MCPlusBuilder {
};
virtual BlocksVectorTy indirectCallPromotion(
- const MCInst &CallInst,
+ const MCInst &CallInst, MCPhysReg Reg,
const std::vector<std::pair<MCSymbol *, uint64_t>> &Targets,
const std::vector<std::pair<MCSymbol *, uint64_t>> &VtableSyms,
const std::vector<MCInst *> &MethodFetchInsns,
diff --git a/bolt/lib/Passes/IndirectCallPromotion.cpp b/bolt/lib/Passes/IndirectCallPromotion.cpp
index 54fd31e37a313..2a89c8c1c005d 100644
--- a/bolt/lib/Passes/IndirectCallPromotion.cpp
+++ b/bolt/lib/Passes/IndirectCallPromotion.cpp
@@ -1142,20 +1142,29 @@ Error IndirectCallPromotion::runOnFunctions(BinaryContext &BC) {
if (opts::ICP == ICP_NONE)
return Error::success();
- if (!BC.isX86()) {
- BC.errs() << "BOLT-ERROR: " << getName() << " is supported only on X86\n";
+ if (!BC.isX86() && !BC.isAArch64()) {
+ BC.errs() << "BOLT-ERROR: " << getName()
+ << " is supported only on X86 and AArch64\n";
exit(1);
}
auto &BFs = BC.getBinaryFunctions();
const bool OptimizeCalls = (opts::ICP == ICP_CALLS || opts::ICP == ICP_ALL);
- const bool OptimizeJumpTables =
+ const bool RequestedJumpTables =
(opts::ICP == ICP_JUMP_TABLES || opts::ICP == ICP_ALL);
+ if (BC.isAArch64() && RequestedJumpTables) {
+ BC.outs()
+ << "BOLT-INFO: ICP jump table promotion is disabled on AArch64\n";
+ if (opts::ICP == ICP_JUMP_TABLES)
+ return Error::success();
+ }
+
+ const bool OptimizeJumpTables = RequestedJumpTables && !BC.isAArch64();
std::unique_ptr<RegAnalysis> RA;
std::unique_ptr<BinaryFunctionCallGraph> CG;
- if (OptimizeJumpTables) {
+ if (OptimizeJumpTables || BC.isAArch64()) {
CG.reset(new BinaryFunctionCallGraph(buildCallGraph(BC)));
RA.reset(new RegAnalysis(BC, &BFs, &*CG));
}
@@ -1369,14 +1378,28 @@ Error IndirectCallPromotion::runOnFunctions(BinaryContext &BC) {
MethodInfo.second.push_back(TargetFetchInst);
}
+ MCPhysReg Reg = 0;
+ if (BC.isAArch64()) {
+ Reg = Info.getLivenessAnalysis().scavengeRegAfter(&Inst);
+ LLVM_DEBUG({
+ dbgs() << "BOLT-DEBUG: ICP ";
+ if (Reg)
+ dbgs() << "found the free register " << BC.MRI->getName(Reg);
+ else
+ dbgs() << "could not find a free register";
+ dbgs() << " to save function address.\n";
+ });
+ }
+
// Generate new promoted call code for this callsite.
MCPlusBuilder::BlocksVectorTy ICPcode =
(IsJumpTable && !opts::ICPJumpTablesByTarget)
? BC.MIB->jumpTablePromotion(Inst, SymTargets,
MethodInfo.second, BC.Ctx.get())
: BC.MIB->indirectCallPromotion(
- Inst, SymTargets, MethodInfo.first, MethodInfo.second,
- opts::ICPOldCodeSequence, BC.Ctx.get());
+ Inst, Reg, SymTargets, MethodInfo.first,
+ MethodInfo.second, opts::ICPOldCodeSequence,
+ BC.Ctx.get());
if (ICPcode.empty()) {
if (opts::Verbosity >= 1)
diff --git a/bolt/lib/Target/AArch64/AArch64MCPlusBuilder.cpp b/bolt/lib/Target/AArch64/AArch64MCPlusBuilder.cpp
index b4a9dff9d25b2..26004c94acdd0 100644
--- a/bolt/lib/Target/AArch64/AArch64MCPlusBuilder.cpp
+++ b/bolt/lib/Target/AArch64/AArch64MCPlusBuilder.cpp
@@ -2281,6 +2281,23 @@ class AArch64MCPlusBuilder : public MCPlusBuilder {
return Code;
}
+ // This helper function creates the snippet of code that compares a register
+ // Reg1 with a register Reg2, and jumps to Target if they are not equal.
+ InstructionListType createCmpJNEWithReg(MCPhysReg Reg1, MCPhysReg Reg2,
+ const MCSymbol *Target,
+ MCContext *Ctx) const override {
+ InstructionListType Code;
+ Code.emplace_back(MCInstBuilder(AArch64::SUBSXrs)
+ .addReg(AArch64::XZR)
+ .addReg(Reg1)
+ .addReg(Reg2)
+ .addImm(0));
+ Code.emplace_back(MCInstBuilder(AArch64::Bcc)
+ .addImm(AArch64CC::NE)
+ .addExpr(MCSymbolRefExpr::create(Target, *Ctx)));
+ return Code;
+ }
+
void createTailCall(MCInst &Inst, const MCSymbol *Target,
MCContext *Ctx) override {
return createDirectCall(Inst, Target, Ctx, /*IsTailCall*/ true);
@@ -3329,6 +3346,136 @@ class AArch64MCPlusBuilder : public MCPlusBuilder {
return Insts;
}
+ BlocksVectorTy indirectCallPromotion(
+ const MCInst &CallInst, MCPhysReg Reg,
+ const std::vector<std::pair<MCSymbol *, uint64_t>> &Targets,
+ const std::vector<std::pair<MCSymbol *, uint64_t>> &VtableSyms,
+ const std::vector<MCInst *> &MethodFetchInsns,
+ const bool MinimizeCodeSize, MCContext *Ctx) override {
+ const bool IsTailCall = isTailCall(CallInst);
+ BlocksVectorTy Results;
+ if (getJumpTable(CallInst))
+ return Results;
+
+ if (!Reg)
+ return Results;
+
+ // Label for the current code block.
+ MCSymbol *NextTarget = nullptr;
+
+ // The join block which contains all the instructions following CallInst.
+ // MergeBlock remains null if CallInst is a tail call.
+ MCSymbol *MergeBlock = nullptr;
+
+ MCPhysReg FuncAddrReg = Reg;
+
+ const bool LoadElim = !VtableSyms.empty();
+ assert((!LoadElim || VtableSyms.size() == Targets.size()) &&
+ "There must be a vtable entry for every method "
+ "in the targets vector.");
+
+ // The compact old-style sequence used by X86 is not implemented for
+ // AArch64 regular call ICP. AArch64 currently emits the default sequence
+ // that branches or calls directly to the hot target.
+ if (MinimizeCodeSize && !LoadElim)
+ return Results;
+
+ assert(CallInst.getOperand(0).isReg() &&
+ "No register was found for indirect call.");
+ const MCPhysReg TargetReg = CallInst.getOperand(0).getReg();
+
+ const auto jumpToMergeBlock = [&](InstructionListType &NewCall) {
+ assert(MergeBlock);
+ NewCall.push_back(CallInst);
+ MCInst &Merge = NewCall.back();
+ Merge.clear();
+ createUncondBranch(Merge, MergeBlock, Ctx);
+ };
+
+ for (unsigned int i = 0; i < Targets.size(); ++i) {
+ Results.emplace_back(NextTarget, InstructionListType());
+ InstructionListType *NewCall = &Results.back().second;
+
+ // Compare current call target to a specific address.
+ assert(Targets[i].first && "All ICP targets must be to known symbols");
+ const MCSymbol *Sym = LoadElim ? VtableSyms[i].first : Targets[i].first;
+ const uint64_t Addend = LoadElim ? VtableSyms[i].second : 0;
+
+ // Materialize the promoted target address.
+ InstructionListType Adr =
+ materializeAddress(Sym, Ctx, FuncAddrReg, Addend);
+ NewCall->insert(NewCall->end(), Adr.begin(), Adr.end());
+
+ // Generate a label for the next compare block.
+ NextTarget = Ctx->createNamedTempSymbol();
+
+ // Jump to the next compare if the target address does not match.
+ InstructionListType CmpJmp =
+ createCmpJNEWithReg(TargetReg, FuncAddrReg, NextTarget, Ctx);
+ NewCall->insert(NewCall->end(), CmpJmp.begin(), CmpJmp.end());
+
+ // Call the promoted target directly.
+ Results.emplace_back(Ctx->createNamedTempSymbol(), InstructionListType());
+ NewCall = &Results.back().second;
+ NewCall->push_back(CallInst);
+ MCInst &CallOrJmp = NewCall->back();
+
+ CallOrJmp.clear();
+ CallOrJmp.setOpcode(IsTailCall ? AArch64::B : AArch64::BL);
+ CallOrJmp.addOperand(MCOperand::createExpr(getTargetExprFor(
+ CallOrJmp, MCSymbolRefExpr::create(Targets[i].first, *Ctx), *Ctx,
+ 0)));
+
+ if (IsTailCall) {
+ setTailCall(CallOrJmp);
+ } else {
+ if (std::optional<uint32_t> Offset = getOffset(CallInst))
+ // Annotated as duplicated call.
+ setOffset(CallOrJmp, *Offset);
+ }
+
+ if (isInvoke(CallInst) && !isInvoke(CallOrJmp)) {
+ // Copy over any EH or GNU args size information from the original call.
+ std::optional<MCPlus::MCLandingPad> EHInfo = getEHInfo(CallInst);
+ if (EHInfo)
+ addEHInfo(CallOrJmp, *EHInfo);
+ int64_t GnuArgsSize = getGnuArgsSize(CallInst);
+ if (GnuArgsSize >= 0)
+ addGnuArgsSize(CallOrJmp, GnuArgsSize);
+ }
+
+ if (!IsTailCall) {
+ // The fallthrough block for the most common target should be the merge
+ // block.
+ if (i == 0) {
+ // Fallthrough to merge block. The CFG will be fixed in the ICP pass.
+ MergeBlock = Ctx->createNamedTempSymbol();
+ } else {
+ // Insert jump to the merge block if we are not doing a fallthrough.
+ jumpToMergeBlock(*NewCall);
+ }
+ }
+ }
+
+ // Cold call block
+ Results.emplace_back(NextTarget, InstructionListType());
+ InstructionListType &NewCall = Results.back().second;
+ for (const MCInst *Inst : MethodFetchInsns)
+ if (Inst != &CallInst)
+ NewCall.push_back(*Inst);
+ NewCall.push_back(CallInst);
+
+ // Jump to merge block from cold call block
+ if (!IsTailCall) {
+ jumpToMergeBlock(NewCall);
+
+ // Record merge block
+ Results.emplace_back(MergeBlock, InstructionListType());
+ }
+
+ return Results;
+ }
+
void createBTI(MCInst &Inst, BTIKind BTI) const override {
Inst.setOpcode(AArch64::HINT);
Inst.clear();
diff --git a/bolt/lib/Target/X86/X86MCPlusBuilder.cpp b/bolt/lib/Target/X86/X86MCPlusBuilder.cpp
index 923de64be58c8..ff7a5524ac791 100644
--- a/bolt/lib/Target/X86/X86MCPlusBuilder.cpp
+++ b/bolt/lib/Target/X86/X86MCPlusBuilder.cpp
@@ -3284,7 +3284,7 @@ class X86MCPlusBuilder : public MCPlusBuilder {
}
BlocksVectorTy indirectCallPromotion(
- const MCInst &CallInst,
+ const MCInst &CallInst, MCPhysReg Reg,
const std::vector<std::pair<MCSymbol *, uint64_t>> &Targets,
const std::vector<std::pair<MCSymbol *, uint64_t>> &VtableSyms,
const std::vector<MCInst *> &MethodFetchInsns,
diff --git a/bolt/test/AArch64/icp-inline.s b/bolt/test/AArch64/icp-inline.s
new file mode 100644
index 0000000000000..b14546fe02a84
--- /dev/null
+++ b/bolt/test/AArch64/icp-inline.s
@@ -0,0 +1,118 @@
+## This test verifies the effect of icp on aarch64 and inline after icp.
+
+## The assembly was produced from C code compiled with clang -O1 -S:
+
+# int foo(int x) { return x + 1; }
+# int bar(int x) { return x*100 + 42; }
+# typedef int (*const fn)(int);
+# fn funcs[] = { foo, bar };
+#
+# int indirectTailCall(int argc) {
+# fn func = funcs[argc];
+# return func(0);
+# }
+# int indirectCall(int argc) {
+# fn func = funcs[argc];
+# int result = func(0);
+# __asm__ volatile("" ::: "memory");
+# return result;
+# }
+
+# RUN: llvm-mc -filetype=obj -triple aarch64-unknown-unknown %s -o %t.o
+# RUN: link_fdata %s %t.o %t.fdata
+# RUN: llvm-strip --strip-unneeded %t.o
+# RUN: %clang %cflags %t.o -o %t.exe -Wl,-q -nostdlib -pie
+
+# AArch64 jump table ICP is disabled until BOLT can reliably recover the
+# jump table index register and limits.
+# RUN: llvm-bolt %t.exe --icp=jump-tables -o %t.null --lite=0 --assume-abi \
+# RUN: --data %t.fdata \
+# RUN: | FileCheck %s --check-prefix=CHECK-ICP-JT-DISABLED
+# CHECK-ICP-JT-DISABLED: BOLT-INFO: ICP jump table promotion is disabled on AArch64
+
+# Indirect call promotion without inline
+# RUN: llvm-bolt %t.exe --icp=calls --icp-calls-topn=1 \
+# RUN: -o %t.null --lite=0 --assume-abi --print-icp --data %t.fdata \
+# RUN: | FileCheck %s --check-prefix=CHECK-ICP-NO-INLINE
+# CHECK-ICP-NO-INLINE: Binary Function "indirectTailCall" after indirect-call-promotion
+# CHECK-ICP-NO-INLINE: b bar
+# CHECK-ICP-NO-INLINE: End of Function "indirectTailCall"
+# CHECK-ICP-NO-INLINE: Binary Function "indirectCall" after indirect-call-promotion
+# CHECK-ICP-NO-INLINE: bl bar
+# CHECK-ICP-NO-INLINE: End of Function "indirectCall"
+
+# Indirect call promotion with inline
+# RUN: llvm-bolt %t.exe --icp=calls --icp-calls-topn=1 --inline-small-functions \
+# RUN: -o %t.null --lite=0 --assume-abi --inline-small-functions-bytes=12 \
+# RUN: --print-inline --data %t.fdata \
+# RUN: | FileCheck %s --check-prefix=CHECK-ICP-WITH-INLINE
+# CHECK-ICP-WITH-INLINE: Binary Function "indirectTailCall" after inlining
+# CHECK-ICP-WITH-INLINE: br x1
+# CHECK-ICP-WITH-INLINE-NOT: b bar
+# CHECK-ICP-WITH-INLINE: End of Function "indirectTailCall"
+ .globl foo
+ .type foo, at function
+foo:
+ .cfi_startproc
+ add w0, w0, #1
+ ret
+ .Lfunc_end0:
+ .size foo, .Lfunc_end0-foo
+ .cfi_endproc
+
+ .globl bar
+ .type bar, at function
+bar:
+ .cfi_startproc
+ mov w8, #100
+ mov x9, #42
+ madd w0, w0, w8, w9
+ ret
+.Lfunc_end1:
+ .size bar, .Lfunc_end1-bar
+ .cfi_endproc
+
+ .globl indirectTailCall
+ .type indirectTailCall, at function
+indirectTailCall:
+ .cfi_startproc
+ adrp x8, funcs
+ add x8, x8, :lo12:funcs
+ ldr x1, [x8, w0, sxtw #3]
+ mov w0, wzr
+ br x1
+# FDATA: 1 indirectTailCall 10 1 foo 0 0 1
+# FDATA: 1 indirectTailCall 10 1 bar 0 0 2
+.Lfunc_end2:
+ .size indirectTailCall, .Lfunc_end2-indirectTailCall
+ .cfi_endproc
+
+ .globl indirectCall
+ .type indirectCall, at function
+indirectCall:
+ .cfi_startproc
+ stp x29, x30, [sp, #-16]!
+ .cfi_def_cfa_offset 16
+ .cfi_offset w29, -16
+ .cfi_offset w30, -8
+ mov x29, sp
+ adrp x8, funcs
+ add x8, x8, :lo12:funcs
+ ldr x1, [x8, w0, sxtw #3]
+ mov w0, wzr
+ blr x1
+# FDATA: 1 indirectCall 18 1 foo 0 0 1
+# FDATA: 1 indirectCall 18 1 bar 0 0 2
+ ldp x29, x30, [sp], #16
+ ret
+.Lfunc_end3:
+ .size indirectCall, .Lfunc_end3-indirectCall
+ .cfi_endproc
+
+ .type funcs, at object
+ .section .data.rel.ro,"aw", at progbits
+ .globl funcs
+funcs:
+ .xword foo
+ .xword bar
+ .size funcs, 16
More information about the llvm-commits
mailing list