[lld] [lld][MachO] Order objc stubs by caller priority (PR #218732)
via llvm-commits
llvm-commits at lists.llvm.org
Tue Aug 25 17:46:48 PDT 2026
llvmorg-github-actions[bot] wrote:
<!--LLVM PR SUMMARY COMMENT-->
@llvm/pr-subscribers-lld
Author: Kyungwoo Lee (kyulee-com)
<details>
<summary>Changes</summary>
__objc_stubs is synthetic, so the input section sorting never reaches its entries. A stub is faulted in when its caller runs, so stubs called from hot code end up scattered across pages that startup otherwise never touches.
Record the sections that branch to each stub, give each stub the lowest priority among its callers, and stable-sort the stubs before addresses are assigned. Stubs with no prioritized caller keep their relative order at the end.
This reduces page faults in __objc_stubs by 40% for a large app.
---
Full diff: https://github.com/llvm/llvm-project/pull/218732.diff
6 Files Affected:
- (modified) lld/MachO/SyntheticSections.cpp (+10)
- (modified) lld/MachO/SyntheticSections.h (+5)
- (modified) lld/MachO/Writer.cpp (+62-2)
- (modified) lld/docs/ReleaseNotes.md (+4)
- (added) lld/test/MachO/objc-stubs-order-file-icf-safe-thunks.s (+63)
- (added) lld/test/MachO/objc-stubs-order-file.s (+90)
``````````diff
diff --git a/lld/MachO/SyntheticSections.cpp b/lld/MachO/SyntheticSections.cpp
index ba06a95bb753c..d3f5ca1409944 100644
--- a/lld/MachO/SyntheticSections.cpp
+++ b/lld/MachO/SyntheticSections.cpp
@@ -944,6 +944,16 @@ uint64_t ObjCStubsSection::getSize() const {
return stubSize * symbols.size();
}
+void ObjCStubsSection::sortSymbols(
+ llvm::function_ref<bool(const Defined *, const Defined *)> less) {
+ auto stubSize = config->objcStubsMode == ObjCStubsMode::fast
+ ? target->objcStubsFastSize
+ : target->objcStubsSmallSize;
+ llvm::stable_sort(symbols, less);
+ for (auto [idx, sym] : llvm::enumerate(symbols))
+ sym->value = idx * stubSize;
+}
+
void ObjCStubsSection::writeTo(uint8_t *buf) const {
uint64_t stubOffset = 0;
for (Defined *sym : symbols) {
diff --git a/lld/MachO/SyntheticSections.h b/lld/MachO/SyntheticSections.h
index e649d1275f821..95f039b893dd6 100644
--- a/lld/MachO/SyntheticSections.h
+++ b/lld/MachO/SyntheticSections.h
@@ -346,6 +346,11 @@ class ObjCStubsSection final : public SyntheticSection {
static bool isObjCStubSymbol(Symbol *sym);
static StringRef getMethname(Symbol *sym);
+ /// Stably sort the stubs by \p less and reassign their offsets. Must run
+ /// before addresses are assigned.
+ void
+ sortSymbols(llvm::function_ref<bool(const Defined *, const Defined *)> less);
+
private:
std::vector<Defined *> symbols;
Symbol *objcMsgSend = nullptr;
diff --git a/lld/MachO/Writer.cpp b/lld/MachO/Writer.cpp
index 89b6d467d0d44..9132a223ce92e 100644
--- a/lld/MachO/Writer.cpp
+++ b/lld/MachO/Writer.cpp
@@ -33,6 +33,7 @@
#include "llvm/Support/xxhash.h"
#include <algorithm>
+#include <limits>
using namespace llvm;
using namespace llvm::MachO;
@@ -43,6 +44,9 @@ using namespace lld::macho;
namespace {
class LCUuid;
+// A section that branches to an objc stub, paired with that stub.
+using ObjCStubCaller = std::pair<const Symbol *, const InputSection *>;
+
class Writer {
public:
Writer() : buffer(errorHandler().outputBuffer) {}
@@ -79,6 +83,11 @@ class Writer {
LCUuid *uuidCommand = nullptr;
OutputSegment *linkEditSegment = nullptr;
+
+ // Recorded while scanning relocations and consumed by
+ // sortSegmentsAndSections(). Only the lowest caller priority is used, so the
+ // pairs are not deduplicated.
+ SmallVector<ObjCStubCaller, 0> objcStubCallers;
};
// LC_DYLD_INFO_ONLY stores the offsets of symbol import/export information.
@@ -703,6 +712,18 @@ void Writer::scanRelocations() {
for (size_t i = 0; i < inputSections.size(); ++i) {
ConcatInputSection *isec = inputSections[i];
+ // Record the original caller section before skipping folded sections. ICF
+ // may canonicalize the caller to a thunk, but the original section still
+ // carries the ObjC stub relocation.
+ for (const Relocation &r : isec->relocs) {
+ if (!target->hasAttr(r.type, RelocAttrBits::BRANCH))
+ continue;
+ auto *sym = dyn_cast_if_present<Symbol *>(r.referent);
+ if (!sym || !ObjCStubsSection::isObjCStubSymbol(sym))
+ continue;
+ objcStubCallers.emplace_back(sym, isec);
+ }
+
if (isec->shouldOmitFromOutput())
continue;
@@ -971,16 +992,55 @@ template <class LP> void Writer::createLoadCommands() {
: 0));
}
+// __objc_stubs is synthetic, so the input section sorting in
+// sortSegmentsAndSections() does not reach its entries. Order each stub by the
+// priority of the earliest-laid-out section that calls it, which keeps the
+// stubs reached during startup together.
+static void orderObjCStubsByCallerPriority(
+ const DenseMap<const InputSection *, int> &priorities,
+ ArrayRef<ObjCStubCaller> callers) {
+ if (priorities.empty() || callers.empty() || !in.objcStubs->isNeeded())
+ return;
+
+ // Sections are laid out in increasing priority, so take the minimum. ICF may
+ // make the prioritized section differ from the section whose relocations
+ // describe the original calls, so use the canonical section for priority
+ // lookup.
+ DenseMap<const Symbol *, int> stubPriority;
+ for (auto [stub, caller] : callers) {
+ auto prio = priorities.find(caller->canonical());
+ if (prio == priorities.end())
+ continue;
+ auto [it, inserted] = stubPriority.try_emplace(stub, prio->second);
+ if (!inserted)
+ it->second = std::min(it->second, prio->second);
+ }
+ if (stubPriority.empty())
+ return;
+
+ auto priorityOf = [&](const Defined *sym) {
+ auto it = stubPriority.find(sym);
+ // Stubs with no prioritized caller sort last, keeping their original order.
+ return it == stubPriority.end() ? std::numeric_limits<int>::max()
+ : it->second;
+ };
+ in.objcStubs->sortSymbols([&](const Defined *a, const Defined *b) {
+ return priorityOf(a) < priorityOf(b);
+ });
+}
+
// Sorting only can happen once all outputs have been collected. Here we sort
// segments, output sections within each segment, and input sections within each
// output segment.
-static void sortSegmentsAndSections() {
+static void sortSegmentsAndSections(ArrayRef<ObjCStubCaller> objcStubCallers) {
TimeTraceScope timeScope("Sort segments and sections");
sortOutputSegments();
DenseMap<const InputSection *, int> isecPriorities =
priorityBuilder.buildInputSectionPriorities();
+ orderObjCStubsByCallerPriority(isecPriorities, objcStubCallers);
+
uint32_t sectionIndex = 0;
for (OutputSegment *seg : outputSegments) {
seg->sortOutputSections();
@@ -1358,7 +1418,7 @@ template <class LP> void Writer::run() {
// hardware call instructions have limited range, e.g., ARM(64).
// The thunks are created as InputSections interspersed among
// the ordinary __TEXT,_text InputSections.
- sortSegmentsAndSections();
+ sortSegmentsAndSections(objcStubCallers);
createLoadCommands<LP>();
finalizeAddresses();
diff --git a/lld/docs/ReleaseNotes.md b/lld/docs/ReleaseNotes.md
index f3cae4a0d97ce..00c9f5c85af8f 100644
--- a/lld/docs/ReleaseNotes.md
+++ b/lld/docs/ReleaseNotes.md
@@ -37,6 +37,10 @@ from the [LLVM releases web site](https://llvm.org/releases/).
### MachO Improvements
+* `__objc_stubs` entries are now ordered by the priority of the sections that
+ call them, so that stubs reached from prioritized code are laid out together.
+ This applies whenever section priorities exist, such as with `-order_file`.
+
### WebAssembly Improvements
* Added support for resolving and merging common data symbols (allocating them
diff --git a/lld/test/MachO/objc-stubs-order-file-icf-safe-thunks.s b/lld/test/MachO/objc-stubs-order-file-icf-safe-thunks.s
new file mode 100644
index 0000000000000..75f721764eee1
--- /dev/null
+++ b/lld/test/MachO/objc-stubs-order-file-icf-safe-thunks.s
@@ -0,0 +1,63 @@
+# REQUIRES: aarch64
+
+## An order-file symbol folded to a safe thunk should still order the ObjC stub
+## called by the original folded body.
+
+# RUN: rm -rf %t && split-file %s %t
+# RUN: llvm-mc -filetype=obj -triple=arm64-apple-darwin %t/input.s -o %t/input.o
+# RUN: %lld -arch arm64 -e _entry -U _objc_msgSend -o %t/out %t/input.o \
+# RUN: -objc_stubs_small --icf=safe_thunks -order_file %t/order
+# RUN: llvm-objdump --no-show-raw-insn -d %t/out | \
+# RUN: FileCheck %s --check-prefix=THUNK
+# RUN: llvm-objdump --no-show-raw-insn --section=__TEXT,__objc_stubs --macho \
+# RUN: %t/out | FileCheck %s --check-prefix=ORDERED
+
+# THUNK-LABEL: <_dup>:
+# THUNK-NEXT: b 0x{{[0-9a-f]+}} <_hot>
+
+# ORDERED: Contents of (__TEXT,__objc_stubs) section
+# ORDERED-NEXT: _objc_msgSend$hot:
+# ORDERED: Objc selector ref: hot
+# ORDERED: _objc_msgSend$cold:
+# ORDERED: Objc selector ref: cold
+# ORDERED: _objc_msgSend$mild:
+# ORDERED: Objc selector ref: mild
+
+#--- input.s
+.text
+.globl _cold
+_cold:
+ bl _objc_msgSend$cold
+ ret
+
+.globl _mild
+_mild:
+ bl _objc_msgSend$mild
+ ret
+
+.globl _hot
+_hot:
+ bl _objc_msgSend$hot
+ ret
+
+.globl _dup
+_dup:
+ bl _objc_msgSend$hot
+ ret
+
+.globl _entry
+_entry:
+ bl _cold
+ bl _mild
+ bl _hot
+ bl _dup
+ ret
+
+.addrsig
+.addrsig_sym _hot
+.addrsig_sym _dup
+
+.subsections_via_symbols
+
+#--- order
+_dup
diff --git a/lld/test/MachO/objc-stubs-order-file.s b/lld/test/MachO/objc-stubs-order-file.s
new file mode 100644
index 0000000000000..0bfe32f1b034c
--- /dev/null
+++ b/lld/test/MachO/objc-stubs-order-file.s
@@ -0,0 +1,90 @@
+# REQUIRES: aarch64
+
+## __objc_stubs is synthetic, so the input section sorting does not reach its
+## entries. Check that they are instead ordered by the priority of the sections
+## that call them.
+
+# RUN: rm -rf %t && split-file %s %t
+# RUN: llvm-mc -filetype=obj -triple=arm64-apple-darwin %t/a.s -o %t/a.o
+
+## Without an order file the stubs keep the order they were interned in.
+# RUN: %lld -arch arm64 -e _main -U _objc_msgSend -o %t/base.out %t/a.o \
+# RUN: -objc_stubs_small
+# RUN: llvm-objdump --no-show-raw-insn --section=__TEXT,__objc_stubs --macho \
+# RUN: %t/base.out | FileCheck %s --check-prefix=BASE
+
+## _hot is ordered first, so the stub it calls moves ahead of the others, which
+## keep their relative order. Each stub must still load its own selector.
+# RUN: %lld -arch arm64 -e _main -U _objc_msgSend -o %t/ordered.out %t/a.o \
+# RUN: -objc_stubs_small -order_file %t/order
+# RUN: llvm-objdump --no-show-raw-insn --section=__TEXT,__objc_stubs --macho \
+# RUN: %t/ordered.out | FileCheck %s --check-prefix=ORDERED
+
+# RUN: %lld -arch arm64 -e _main -U _objc_msgSend -o %t/fast.out %t/a.o \
+# RUN: -objc_stubs_fast -order_file %t/order
+# RUN: llvm-objdump --no-show-raw-insn --section=__TEXT,__objc_stubs --macho \
+# RUN: %t/fast.out | FileCheck %s --check-prefix=ORDERED
+
+## Callers are recorded after ICF. _dup is identical to _hot and folds with it,
+## so the priority has to reach the stub through the surviving section.
+# RUN: %lld -arch arm64 -e _main -U _objc_msgSend -o %t/icf.out %t/a.o \
+# RUN: -objc_stubs_small -order_file %t/order --icf=all
+# RUN: llvm-nm --numeric-sort %t/icf.out | FileCheck %s --check-prefix=FOLDED
+# RUN: llvm-objdump --no-show-raw-insn --section=__TEXT,__objc_stubs --macho \
+# RUN: %t/icf.out | FileCheck %s --check-prefix=ORDERED
+
+## Check that _dup folded into _hot.
+# FOLDED: [[ADDR:[0-9a-f]+]] T _dup
+# FOLDED-NEXT: [[ADDR]] T _hot
+
+# BASE: Contents of (__TEXT,__objc_stubs) section
+# BASE-NEXT: _objc_msgSend$cold:
+# BASE: _objc_msgSend$hot:
+# BASE: _objc_msgSend$mild:
+
+# ORDERED: Contents of (__TEXT,__objc_stubs) section
+# ORDERED-NEXT: _objc_msgSend$hot:
+# ORDERED: Objc selector ref: hot
+# ORDERED: _objc_msgSend$cold:
+# ORDERED: Objc selector ref: cold
+# ORDERED: _objc_msgSend$mild:
+# ORDERED: Objc selector ref: mild
+
+#--- a.s
+.text
+.globl _cold
+.p2align 2
+_cold:
+ bl _objc_msgSend$cold
+ ret
+
+.globl _mild
+.p2align 2
+_mild:
+ bl _objc_msgSend$mild
+ ret
+
+.globl _hot
+.p2align 2
+_hot:
+ bl _objc_msgSend$hot
+ ret
+
+.globl _dup
+.p2align 2
+_dup:
+ bl _objc_msgSend$hot
+ ret
+
+.globl _main
+.p2align 2
+_main:
+ bl _cold
+ bl _mild
+ bl _hot
+ ret
+
+.subsections_via_symbols
+
+#--- order
+_hot
``````````
</details>
https://github.com/llvm/llvm-project/pull/218732
More information about the llvm-commits
mailing list