[lld] [lld][MachO] Add initializer and +load roots to temporal BP sorting (PR #212110)

Karim Alweheshy via llvm-commits llvm-commits at lists.llvm.org
Tue Aug 4 22:34:42 PDT 2026


https://github.com/karim-alweheshy updated https://github.com/llvm/llvm-project/pull/212110

>From ea8022b2b9679281dc5cd5c1addf01ae699b54ca Mon Sep 17 00:00:00 2001
From: Karim Alweheshy <karim.alweheshy at gmail.com>
Date: Sun, 26 Jul 2026 12:44:08 +0200
Subject: [PATCH 1/3] [lld][MachO] Add initializers to temporal BP startup
 sorting

---
 lld/ELF/BPSectionOrderer.cpp                  |  3 +-
 lld/MachO/BPSectionOrderer.cpp                | 30 +++++++-
 lld/MachO/BPSectionOrderer.h                  |  2 +-
 lld/MachO/Config.h                            |  1 +
 lld/MachO/Driver.cpp                          |  6 ++
 lld/MachO/Options.td                          |  6 ++
 lld/MachO/SectionPriorities.cpp               |  2 +-
 .../lld/Common/BPSectionOrdererBase.inc       | 11 ++-
 .../MachO/bp-section-orderer-initializers.s   | 70 +++++++++++++++++++
 9 files changed, 124 insertions(+), 7 deletions(-)
 create mode 100644 lld/test/MachO/bp-section-orderer-initializers.s

diff --git a/lld/ELF/BPSectionOrderer.cpp b/lld/ELF/BPSectionOrderer.cpp
index 0ce7ceff4348b..c9bdc80c3d093 100644
--- a/lld/ELF/BPSectionOrderer.cpp
+++ b/lld/ELF/BPSectionOrderer.cpp
@@ -98,5 +98,6 @@ DenseMap<const InputSectionBase *, int> elf::runBalancedPartitioning(
   return orderer.computeOrder(profilePath, compressionSortSpecs,
                               forFunctionCompression, forDataCompression,
                               compressionSortStartupFunctions, verbose,
-                              sections, rootSymbolToSectionIdxs);
+                              sections, rootSymbolToSectionIdxs,
+                              /*initialStartupSectionIdxs=*/{});
 }
diff --git a/lld/MachO/BPSectionOrderer.cpp b/lld/MachO/BPSectionOrderer.cpp
index 6da9985006247..5c9f5e93cc2dc 100644
--- a/lld/MachO/BPSectionOrderer.cpp
+++ b/lld/MachO/BPSectionOrderer.cpp
@@ -12,10 +12,12 @@
 #include "Relocations.h"
 #include "Symbols.h"
 #include "Target.h"
+#include "SyntheticSections.h"
 #include "lld/Common/BPSectionOrdererBase.inc"
 #include "llvm/ADT/DenseMap.h"
 #include "llvm/ADT/STLExtras.h"
 #include "llvm/ADT/StableHashing.h"
+#include "llvm/BinaryFormat/MachO.h"
 #include "llvm/Support/Endian.h"
 #include "llvm/Support/xxhash.h"
 
@@ -118,7 +120,7 @@ struct BPOrdererMachO : lld::BPOrderer<BPOrdererMachO> {
 DenseMap<const InputSection *, int> lld::macho::runBalancedPartitioning(
     StringRef profilePath, ArrayRef<BPCompressionSortSpec> compressionSortSpecs,
     bool forFunctionCompression, bool forDataCompression,
-    bool compressionSortStartupFunctions, bool verbose) {
+    bool compressionSortStartupFunctions, bool sortInitializers, bool verbose) {
   // Collect candidate sections and associated symbols.
   SmallVector<InputSection *> sections;
   DenseMap<const InputSection *, unsigned> sectionToIdx;
@@ -182,10 +184,34 @@ DenseMap<const InputSection *, int> lld::macho::runBalancedPartitioning(
     }
   }
 
+  SetVector<unsigned> initialStartupSectionIdxs;
+  if (sortInitializers) {
+    const auto &initOffsetInputs = in.initOffsets->inputs();
+    SmallVector<ConcatInputSection *> initializerSections(
+        initOffsetInputs.begin(), initOffsetInputs.end());
+    for (InputSection *isec : inputSections)
+      if (sectionType(isec->getFlags()) ==
+          llvm::MachO::S_MOD_INIT_FUNC_POINTERS)
+        initializerSections.push_back(cast<ConcatInputSection>(isec));
+    for (const ConcatInputSection *isec : initializerSections) {
+      for (const Relocation &reloc : isec->relocs) {
+        InputSection *referent = reloc.getReferentInputSection();
+        if (!referent)
+          continue;
+        auto it = sectionToIdx.find(referent->canonical());
+        if (it != sectionToIdx.end())
+          initialStartupSectionIdxs.insert(it->second);
+      }
+    }
+    if (verbose)
+      dbgs() << "Initializer functions for startup: "
+             << initialStartupSectionIdxs.size() << "\n";
+  }
+
   auto result = BPOrdererMachO().computeOrder(
       profilePath, compressionSortSpecs, forFunctionCompression,
       forDataCompression, compressionSortStartupFunctions, verbose, sections,
-      rootSymbolToSectionIdxs);
+      rootSymbolToSectionIdxs, initialStartupSectionIdxs.getArrayRef());
   // BP already orders cold sections after non-cold via separate buckets.
   // Unset isCold on sections that received a BP priority so Writer.cpp's
   // stable_partition doesn't re-partition them. Sections without a BP priority
diff --git a/lld/MachO/BPSectionOrderer.h b/lld/MachO/BPSectionOrderer.h
index cc63c85cd32d3..e4b8dd0575913 100644
--- a/lld/MachO/BPSectionOrderer.h
+++ b/lld/MachO/BPSectionOrderer.h
@@ -31,7 +31,7 @@ llvm::DenseMap<const InputSection *, int> runBalancedPartitioning(
     llvm::StringRef profilePath,
     llvm::ArrayRef<BPCompressionSortSpec> compressionSortSpecs,
     bool forFunctionCompression, bool forDataCompression,
-    bool compressionSortStartupFunctions, bool verbose);
+    bool compressionSortStartupFunctions, bool sortInitializers, bool verbose);
 
 } // namespace lld::macho
 
diff --git a/lld/MachO/Config.h b/lld/MachO/Config.h
index 9767cc5e5b6e4..6048b8ea3365c 100644
--- a/lld/MachO/Config.h
+++ b/lld/MachO/Config.h
@@ -233,6 +233,7 @@ struct Configuration {
 
   llvm::StringRef irpgoProfilePath;
   bool bpStartupFunctionSort = false;
+  bool bpStartupSortInitializers = false;
   bool bpCompressionSortStartupFunctions = false;
   bool bpFunctionOrderForCompression = false;
   bool bpDataOrderForCompression = false;
diff --git a/lld/MachO/Driver.cpp b/lld/MachO/Driver.cpp
index 2864c6d28fa49..a75576af51fc6 100644
--- a/lld/MachO/Driver.cpp
+++ b/lld/MachO/Driver.cpp
@@ -2102,6 +2102,9 @@ bool link(ArrayRef<const char *> argsArr, llvm::raw_ostream &stdoutOS,
   config->bpCompressionSortStartupFunctions =
       args.hasFlag(OPT_bp_compression_sort_startup_functions,
                    OPT_no_bp_compression_sort_startup_functions, false);
+  config->bpStartupSortInitializers =
+      args.hasFlag(OPT_bp_startup_sort_initializers,
+                   OPT_no_bp_startup_sort_initializers, false);
   if (const Arg *arg = args.getLastArg(OPT_bp_startup_sort)) {
     StringRef startupSortStr = arg->getValue();
     if (startupSortStr == "function") {
@@ -2116,6 +2119,9 @@ bool link(ArrayRef<const char *> argsArr, llvm::raw_ostream &stdoutOS,
       config->bpCompressionSortStartupFunctions)
     error("--bp-compression-sort-startup-functions must be used with "
           "--bp-startup-sort=function");
+  if (!config->bpStartupFunctionSort && config->bpStartupSortInitializers)
+    error("--bp-startup-sort-initializers must be used with "
+          "--bp-startup-sort=function");
   if (config->irpgoProfilePath.empty() && config->bpStartupFunctionSort)
     error("--bp-startup-sort=function must be used with "
           "--irpgo-profile");
diff --git a/lld/MachO/Options.td b/lld/MachO/Options.td
index b7686d66a258e..72da037f2d8d4 100644
--- a/lld/MachO/Options.td
+++ b/lld/MachO/Options.td
@@ -142,6 +142,12 @@ def bp_startup_sort: Joined<["--"], "bp-startup-sort=">,
     MetaVarName<"[none,function]">,
     HelpText<"Order sections based on profile data to improve startup time">,
     Group<grp_lld>;
+def bp_startup_sort_initializers: Flag<["--"], "bp-startup-sort-initializers">,
+    HelpText<"Include Mach-O initializer functions in balanced-partitioning startup sorting">,
+    Group<grp_lld>;
+def no_bp_startup_sort_initializers: Flag<["--"], "no-bp-startup-sort-initializers">,
+    HelpText<"Do not include Mach-O initializer functions in balanced-partitioning startup sorting">,
+    Group<grp_lld>;
 def bp_compression_sort_startup_functions: Flag<["--"], "bp-compression-sort-startup-functions">,
     HelpText<"Order startup functions to improve compressed size in addition to startup time">,
     Group<grp_lld>;
diff --git a/lld/MachO/SectionPriorities.cpp b/lld/MachO/SectionPriorities.cpp
index dea47c76a385e..481d69eca52f1 100644
--- a/lld/MachO/SectionPriorities.cpp
+++ b/lld/MachO/SectionPriorities.cpp
@@ -381,7 +381,7 @@ macho::PriorityBuilder::buildInputSectionPriorities() {
         config->bpCompressionSortSpecs, config->bpFunctionOrderForCompression,
         config->bpDataOrderForCompression,
         config->bpCompressionSortStartupFunctions,
-        config->bpVerboseSectionOrderer);
+        config->bpStartupSortInitializers, config->bpVerboseSectionOrderer);
   } else if (config->callGraphProfileSort) {
     // Sort sections by the profile data provided by __LLVM,__cg_profile
     // sections.
diff --git a/lld/include/lld/Common/BPSectionOrdererBase.inc b/lld/include/lld/Common/BPSectionOrdererBase.inc
index b2e87d6c240fc..53f45dd7fbb75 100644
--- a/lld/include/lld/Common/BPSectionOrdererBase.inc
+++ b/lld/include/lld/Common/BPSectionOrdererBase.inc
@@ -68,7 +68,8 @@ template <class D> struct BPOrderer {
                     bool compressionSortStartupFunctions, bool verbose,
                     llvm::ArrayRef<Section *> sections,
                     const DenseMap<CachedHashStringRef, std::set<unsigned>>
-                        &rootSymbolToSectionIdxs)
+                        &rootSymbolToSectionIdxs,
+                    llvm::ArrayRef<unsigned> initialStartupSectionIdxs)
       -> llvm::DenseMap<const Section *, int>;
 
   std::optional<StringRef> static getResolvedLinkageName(StringRef name) {
@@ -164,7 +165,9 @@ auto BPOrderer<D>::computeOrder(
     bool compressionSortStartupFunctions, bool verbose,
     ArrayRef<Section *> sections,
     const DenseMap<CachedHashStringRef, std::set<unsigned>>
-        &rootSymbolToSectionIdxs) -> DenseMap<const Section *, int> {
+        &rootSymbolToSectionIdxs,
+    ArrayRef<unsigned> initialStartupSectionIdxs)
+    -> DenseMap<const Section *, int> {
   TimeTraceScope timeScope("Setup Balanced Partitioning");
   DenseMap<const void *, uint64_t> sectionToIdx;
   for (auto [i, isec] : llvm::enumerate(sections))
@@ -197,6 +200,10 @@ auto BPOrderer<D>::computeOrder(
     for (size_t traceIdx = 0; traceIdx < traces.size(); traceIdx++) {
       uint64_t currentSize = 0, cutoffSize = 1;
       size_t cutoffTimestamp = 1;
+      for (unsigned sectionIdx : initialStartupSectionIdxs) {
+        sectionIdxToTimestamp.try_emplace(sectionIdx, 0);
+        sectionIdxToFirstUN.try_emplace(sectionIdx, maxUN);
+      }
       auto &trace = traces[traceIdx].FunctionNameRefs;
       for (size_t timestamp = 0; timestamp < trace.size(); timestamp++) {
         auto [_, parsedFuncName] = getParsedIRPGOName(
diff --git a/lld/test/MachO/bp-section-orderer-initializers.s b/lld/test/MachO/bp-section-orderer-initializers.s
new file mode 100644
index 0000000000000..9ae5cc5dde2f4
--- /dev/null
+++ b/lld/test/MachO/bp-section-orderer-initializers.s
@@ -0,0 +1,70 @@
+# REQUIRES: aarch64
+
+# 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: llvm-profdata merge %t/profile.proftext -o %t/profile.profdata
+
+## The initializer is absent from the temporal profile, but dyld executes it
+## before main. The opt-in initializer seed includes it in the startup order.
+# RUN: %lld -arch arm64 -lSystem -e _main -o %t/baseline %t/input.o --irpgo-profile=%t/profile.profdata --bp-startup-sort=function --bp-compression-sort=none --verbose-bp-section-orderer 2>&1 | FileCheck %s --check-prefix=BASELINE
+# BASELINE: Ordered 1 sections (8 bytes) using balanced partitioning
+# BASELINE: Functions for startup: 1 (8 bytes)
+
+# RUN: %lld -arch arm64 -lSystem -e _main -o - %t/input.o --irpgo-profile=%t/profile.profdata --bp-startup-sort=function --bp-startup-sort-initializers --bp-compression-sort=none --verbose-bp-section-orderer 2>&1 | FileCheck %s --check-prefix=INITIALIZERS
+# INITIALIZERS: Initializer functions for startup: 1
+# INITIALIZERS: Ordered 2 sections (16 bytes) using balanced partitioning
+# INITIALIZERS: Functions for startup: 2 (16 bytes)
+
+# RUN: %lld -arch arm64 -lSystem -e _main -o - %t/input.o --irpgo-profile=%t/profile.profdata --bp-startup-sort=function --bp-startup-sort-initializers --bp-compression-sort=none | llvm-nm --numeric-sort --format=just-symbols - | FileCheck %s --check-prefix=ORDER
+# ORDER: _hot
+# ORDER-NEXT: _initializer
+# ORDER-NEXT: _main
+# ORDER-NEXT: _cold
+
+# RUN: not %lld -arch arm64 -lSystem -e _main -o /dev/null %t/input.o --bp-startup-sort-initializers 2>&1 | FileCheck %s --check-prefix=ERROR
+# ERROR: --bp-startup-sort-initializers must be used with --bp-startup-sort=function
+
+#--- input.s
+.subsections_via_symbols
+
+.text
+.globl _main
+_main:
+  ret
+
+.globl _cold
+_cold:
+  mov w0, #1
+  ret
+
+.globl _hot
+_hot:
+  mov w0, #2
+  ret
+
+.globl _initializer
+_initializer:
+  mov w0, #3
+  ret
+
+.section __DATA,__mod_init_func,mod_init_funcs
+.quad _initializer
+
+#--- profile.proftext
+:ir
+:temporal_prof_traces
+# Num Traces
+1
+# Trace Stream Size:
+1
+# Weight
+1
+hot,
+
+hot
+# Func Hash:
+1111
+# Num Counters:
+1
+# Counter Values:
+1

>From 76d0d371c4fde5f08b308ac5ff073468651d87dc Mon Sep 17 00:00:00 2001
From: Karim Alweheshy <karim.alweheshy at gmail.com>
Date: Mon, 27 Jul 2026 00:23:33 +0200
Subject: [PATCH 2/3] [lld][MachO] Add Objective-C +load roots to temporal BP

---
 lld/ELF/BPSectionOrderer.cpp                  |   3 +-
 lld/MachO/BPSectionOrderer.cpp                |  24 ++-
 lld/MachO/BPSectionOrderer.h                  |   3 +-
 lld/MachO/Config.h                            |   1 +
 lld/MachO/Driver.cpp                          |   6 +
 lld/MachO/ObjC.cpp                            | 131 ++++++++++++++
 lld/MachO/ObjC.h                              |   4 +
 lld/MachO/Options.td                          |   6 +
 lld/MachO/SectionPriorities.cpp               |   3 +-
 .../lld/Common/BPSectionOrdererBase.inc       |  13 +-
 .../MachO/bp-startup-sort-objc-load-methods.s | 162 ++++++++++++++++++
 11 files changed, 349 insertions(+), 7 deletions(-)
 create mode 100644 lld/test/MachO/bp-startup-sort-objc-load-methods.s

diff --git a/lld/ELF/BPSectionOrderer.cpp b/lld/ELF/BPSectionOrderer.cpp
index c9bdc80c3d093..671c63ced97a8 100644
--- a/lld/ELF/BPSectionOrderer.cpp
+++ b/lld/ELF/BPSectionOrderer.cpp
@@ -99,5 +99,6 @@ DenseMap<const InputSectionBase *, int> elf::runBalancedPartitioning(
                               forFunctionCompression, forDataCompression,
                               compressionSortStartupFunctions, verbose,
                               sections, rootSymbolToSectionIdxs,
-                              /*initialStartupSectionIdxs=*/{});
+                              /*initialStartupSectionIdxs=*/{},
+                              /*prefixStartupSectionIdxs=*/{});
 }
diff --git a/lld/MachO/BPSectionOrderer.cpp b/lld/MachO/BPSectionOrderer.cpp
index 5c9f5e93cc2dc..a3daeec9b9b70 100644
--- a/lld/MachO/BPSectionOrderer.cpp
+++ b/lld/MachO/BPSectionOrderer.cpp
@@ -8,6 +8,7 @@
 
 #include "BPSectionOrderer.h"
 #include "InputSection.h"
+#include "ObjC.h"
 #include "OutputSegment.h"
 #include "Relocations.h"
 #include "Symbols.h"
@@ -120,7 +121,8 @@ struct BPOrdererMachO : lld::BPOrderer<BPOrdererMachO> {
 DenseMap<const InputSection *, int> lld::macho::runBalancedPartitioning(
     StringRef profilePath, ArrayRef<BPCompressionSortSpec> compressionSortSpecs,
     bool forFunctionCompression, bool forDataCompression,
-    bool compressionSortStartupFunctions, bool sortInitializers, bool verbose) {
+    bool compressionSortStartupFunctions, bool sortInitializers,
+    bool sortObjCLoadMethods, bool verbose) {
   // Collect candidate sections and associated symbols.
   SmallVector<InputSection *> sections;
   DenseMap<const InputSection *, unsigned> sectionToIdx;
@@ -208,10 +210,28 @@ DenseMap<const InputSection *, int> lld::macho::runBalancedPartitioning(
              << initialStartupSectionIdxs.size() << "\n";
   }
 
+  SmallVector<unsigned> objcLoadSectionIdxs;
+  if (sortObjCLoadMethods) {
+    for (InputSection *isec : objc::getLoadMethodSections()) {
+      auto it = sectionToIdx.find(isec);
+      if (it != sectionToIdx.end()) {
+        initialStartupSectionIdxs.insert(it->second);
+        objcLoadSectionIdxs.push_back(it->second);
+      }
+    }
+    llvm::sort(objcLoadSectionIdxs);
+    objcLoadSectionIdxs.erase(llvm::unique(objcLoadSectionIdxs),
+                              objcLoadSectionIdxs.end());
+    if (verbose)
+      dbgs() << "Objective-C +load functions for startup: "
+             << objcLoadSectionIdxs.size() << "\n";
+  }
+
   auto result = BPOrdererMachO().computeOrder(
       profilePath, compressionSortSpecs, forFunctionCompression,
       forDataCompression, compressionSortStartupFunctions, verbose, sections,
-      rootSymbolToSectionIdxs, initialStartupSectionIdxs.getArrayRef());
+      rootSymbolToSectionIdxs, initialStartupSectionIdxs.getArrayRef(),
+      objcLoadSectionIdxs);
   // BP already orders cold sections after non-cold via separate buckets.
   // Unset isCold on sections that received a BP priority so Writer.cpp's
   // stable_partition doesn't re-partition them. Sections without a BP priority
diff --git a/lld/MachO/BPSectionOrderer.h b/lld/MachO/BPSectionOrderer.h
index e4b8dd0575913..69dc8b9acb47f 100644
--- a/lld/MachO/BPSectionOrderer.h
+++ b/lld/MachO/BPSectionOrderer.h
@@ -31,7 +31,8 @@ llvm::DenseMap<const InputSection *, int> runBalancedPartitioning(
     llvm::StringRef profilePath,
     llvm::ArrayRef<BPCompressionSortSpec> compressionSortSpecs,
     bool forFunctionCompression, bool forDataCompression,
-    bool compressionSortStartupFunctions, bool sortInitializers, bool verbose);
+    bool compressionSortStartupFunctions, bool sortInitializers,
+    bool sortObjCLoadMethods, bool verbose);
 
 } // namespace lld::macho
 
diff --git a/lld/MachO/Config.h b/lld/MachO/Config.h
index 6048b8ea3365c..a66f193ef68ac 100644
--- a/lld/MachO/Config.h
+++ b/lld/MachO/Config.h
@@ -234,6 +234,7 @@ struct Configuration {
   llvm::StringRef irpgoProfilePath;
   bool bpStartupFunctionSort = false;
   bool bpStartupSortInitializers = false;
+  bool bpStartupSortObjCLoadMethods = false;
   bool bpCompressionSortStartupFunctions = false;
   bool bpFunctionOrderForCompression = false;
   bool bpDataOrderForCompression = false;
diff --git a/lld/MachO/Driver.cpp b/lld/MachO/Driver.cpp
index a75576af51fc6..f7bb70d0daec4 100644
--- a/lld/MachO/Driver.cpp
+++ b/lld/MachO/Driver.cpp
@@ -2105,6 +2105,9 @@ bool link(ArrayRef<const char *> argsArr, llvm::raw_ostream &stdoutOS,
   config->bpStartupSortInitializers =
       args.hasFlag(OPT_bp_startup_sort_initializers,
                    OPT_no_bp_startup_sort_initializers, false);
+  config->bpStartupSortObjCLoadMethods =
+      args.hasFlag(OPT_bp_startup_sort_objc_load_methods,
+                   OPT_no_bp_startup_sort_objc_load_methods, false);
   if (const Arg *arg = args.getLastArg(OPT_bp_startup_sort)) {
     StringRef startupSortStr = arg->getValue();
     if (startupSortStr == "function") {
@@ -2122,6 +2125,9 @@ bool link(ArrayRef<const char *> argsArr, llvm::raw_ostream &stdoutOS,
   if (!config->bpStartupFunctionSort && config->bpStartupSortInitializers)
     error("--bp-startup-sort-initializers must be used with "
           "--bp-startup-sort=function");
+  if (!config->bpStartupFunctionSort && config->bpStartupSortObjCLoadMethods)
+    error("--bp-startup-sort-objc-load-methods must be used with "
+          "--bp-startup-sort=function");
   if (config->irpgoProfilePath.empty() && config->bpStartupFunctionSort)
     error("--bp-startup-sort=function must be used with "
           "--irpgo-profile");
diff --git a/lld/MachO/ObjC.cpp b/lld/MachO/ObjC.cpp
index 6a0f2f6347991..25e5536c0f322 100644
--- a/lld/MachO/ObjC.cpp
+++ b/lld/MachO/ObjC.cpp
@@ -18,6 +18,8 @@
 #include "lld/Common/ErrorHandler.h"
 #include "llvm/ADT/DenseMap.h"
 #include "llvm/Bitcode/BitcodeReader.h"
+#include "llvm/Support/Endian.h"
+#include "llvm/Support/MathExtras.h"
 #include "llvm/Support/TimeProfiler.h"
 
 using namespace llvm;
@@ -323,6 +325,135 @@ void objc::checkCategories() {
   }
 }
 
+SmallVector<InputSection *> objc::getLoadMethodSections() {
+  CategoryLayout catLayout(target->wordSize);
+  ClassLayout classLayout(target->wordSize);
+  ROClassLayout roClassLayout(target->wordSize);
+  ListHeaderLayout listHeaderLayout(target->wordSize);
+  MethodLayout methodLayout(target->wordSize);
+  SetVector<InputSection *> loadMethods;
+
+  struct SectionLocation {
+    InputSection *isec = nullptr;
+    uint64_t offset = 0;
+  };
+
+  auto getRelocationTarget = [](const Relocation *reloc,
+                                int64_t extraAddend = 0) -> SectionLocation {
+    if (!reloc)
+      return {};
+    InputSection *isec = reloc->getReferentInputSection();
+    if (!isec)
+      return {};
+    auto [offset, overflow] =
+        llvm::AddOverflow<int64_t>(reloc->addend, extraAddend);
+    if (overflow)
+      return {};
+    if (auto *sym = reloc->referent.dyn_cast<Symbol *>()) {
+      auto *defined = dyn_cast<Defined>(sym);
+      if (!defined || defined->value > INT64_MAX)
+        return {};
+      auto added = llvm::AddOverflow<int64_t>(
+          offset, static_cast<int64_t>(defined->value));
+      if (added.second)
+        return {};
+      offset = added.first;
+    }
+    if (offset < 0 || static_cast<uint64_t>(offset) > isec->data.size())
+      return {};
+    return {isec, static_cast<uint64_t>(offset)};
+  };
+
+  auto getRelocationTargetAt = [&](InputSection *isec,
+                                   uint32_t offset) -> SectionLocation {
+    const Relocation *reloc = isec->getRelocAt(offset);
+    if (!reloc || !target->hasAttr(reloc->type, RelocAttrBits::SUBTRAHEND))
+      return getRelocationTarget(reloc);
+
+    SectionLocation subtrahend = getRelocationTarget(reloc);
+    // parseRelocations stores the paired UNSIGNED minuend immediately after
+    // the SUBTRACTOR; the suffix does not carry the relocation field offset.
+    const Relocation *minuend = reloc + 1;
+    if (!subtrahend.isec || subtrahend.isec != isec ||
+        subtrahend.offset > INT64_MAX)
+      return {};
+    int64_t extraAddend =
+        static_cast<int64_t>(offset) - static_cast<int64_t>(subtrahend.offset);
+    return getRelocationTarget(minuend, extraAddend);
+  };
+
+  auto getReferent = [&](SectionLocation location,
+                         uint32_t fieldOffset) -> SectionLocation {
+    if (!location.isec || location.offset + fieldOffset > UINT32_MAX ||
+        location.offset + fieldOffset >= location.isec->data.size())
+      return {};
+    return getRelocationTargetAt(
+        location.isec, static_cast<uint32_t>(location.offset + fieldOffset));
+  };
+
+  auto getString = [](SectionLocation location) -> StringRef {
+    if (!location.isec || location.offset >= location.isec->data.size())
+      return {};
+    if (auto *strings = dyn_cast<CStringInputSection>(location.isec))
+      return strings->getStringRefAtOffset(location.offset);
+    ArrayRef<uint8_t> data = location.isec->data.slice(location.offset);
+    const char *string = reinterpret_cast<const char *>(data.data());
+    return StringRef(string, strnlen(string, data.size()));
+  };
+
+  auto collectLoadMethod = [&](SectionLocation methods) {
+    if (!methods.isec ||
+        methods.offset + listHeaderLayout.totalSize > methods.isec->data.size())
+      return;
+    const uint8_t *header = methods.isec->data.data() + methods.offset;
+    uint32_t sizeAndFlags = support::endian::read32le(header);
+    uint32_t entrySize = sizeAndFlags & 0xffff;
+    uint32_t entryCount = support::endian::read32le(header + sizeof(uint32_t));
+    bool relative = sizeAndFlags & 0x80000000;
+    uint32_t nameOffset = relative ? 0 : methodLayout.nameOffset;
+    uint32_t implOffset =
+        relative ? 2 * sizeof(uint32_t) : methodLayout.implOffset;
+    uint64_t entriesOffset = methods.offset + listHeaderLayout.totalSize;
+    uint64_t entriesSize = static_cast<uint64_t>(entrySize) * entryCount;
+    if (entrySize <= std::max(nameOffset, implOffset) ||
+        entriesOffset + entriesSize > methods.isec->data.size())
+      return;
+
+    for (uint32_t index = 0; index < entryCount; ++index) {
+      uint64_t entryOffset =
+          entriesOffset + static_cast<uint64_t>(index) * entrySize;
+      SectionLocation name = getRelocationTargetAt(
+          methods.isec, static_cast<uint32_t>(entryOffset + nameOffset));
+      if (getString(name) != "load")
+        continue;
+      InputSection *impl =
+          getRelocationTargetAt(methods.isec,
+                                static_cast<uint32_t>(entryOffset + implOffset))
+              .isec;
+      if (impl && isCodeSection(impl))
+        loadMethods.insert(impl->canonical());
+    }
+  };
+
+  auto getClassMethods = [&](SectionLocation klass) {
+    SectionLocation metaClass = getReferent(klass, classLayout.metaClassOffset);
+    SectionLocation roClass = getReferent(metaClass, classLayout.roDataOffset);
+    return getReferent(roClass, roClassLayout.baseMethodsOffset);
+  };
+
+  for (InputSection *listIsec : inputSections) {
+    if (listIsec->getName() == section_names::objcNonLazyClassList) {
+      for (const Relocation &reloc : listIsec->relocs)
+        collectLoadMethod(getClassMethods(getRelocationTarget(&reloc)));
+    } else if (listIsec->getName() == section_names::objcNonLazyCatList) {
+      for (const Relocation &reloc : listIsec->relocs)
+        collectLoadMethod(getReferent(getRelocationTarget(&reloc),
+                                      catLayout.classMethodsOffset));
+    }
+  }
+  return SmallVector<InputSection *>(loadMethods.begin(), loadMethods.end());
+}
+
 namespace {
 
 class ObjcCategoryMerger {
diff --git a/lld/MachO/ObjC.h b/lld/MachO/ObjC.h
index db259a82fbbdb..b9aafeb0cd9f6 100644
--- a/lld/MachO/ObjC.h
+++ b/lld/MachO/ObjC.h
@@ -9,10 +9,13 @@
 #ifndef LLD_MACHO_OBJC_H
 #define LLD_MACHO_OBJC_H
 
+#include "llvm/ADT/SmallVector.h"
 #include "llvm/Support/MemoryBuffer.h"
 
 namespace lld::macho {
 
+class InputSection;
+
 namespace objc {
 
 namespace symbol_names {
@@ -40,6 +43,7 @@ constexpr const char swift_objc_klass[] = "_$s";
 // Check for duplicate method names within related categories / classes.
 void checkCategories();
 void mergeCategories();
+llvm::SmallVector<InputSection *> getLoadMethodSections();
 
 void doCleanup();
 } // namespace objc
diff --git a/lld/MachO/Options.td b/lld/MachO/Options.td
index 72da037f2d8d4..696bf2649707d 100644
--- a/lld/MachO/Options.td
+++ b/lld/MachO/Options.td
@@ -148,6 +148,12 @@ def bp_startup_sort_initializers: Flag<["--"], "bp-startup-sort-initializers">,
 def no_bp_startup_sort_initializers: Flag<["--"], "no-bp-startup-sort-initializers">,
     HelpText<"Do not include Mach-O initializer functions in balanced-partitioning startup sorting">,
     Group<grp_lld>;
+def bp_startup_sort_objc_load_methods: Flag<["--"], "bp-startup-sort-objc-load-methods">,
+    HelpText<"Include Objective-C +load implementations as a balanced-partitioning startup prefix">,
+    Group<grp_lld>;
+def no_bp_startup_sort_objc_load_methods: Flag<["--"], "no-bp-startup-sort-objc-load-methods">,
+    HelpText<"Do not include Objective-C +load implementations as a balanced-partitioning startup prefix">,
+    Group<grp_lld>;
 def bp_compression_sort_startup_functions: Flag<["--"], "bp-compression-sort-startup-functions">,
     HelpText<"Order startup functions to improve compressed size in addition to startup time">,
     Group<grp_lld>;
diff --git a/lld/MachO/SectionPriorities.cpp b/lld/MachO/SectionPriorities.cpp
index 481d69eca52f1..24d36d763e7c8 100644
--- a/lld/MachO/SectionPriorities.cpp
+++ b/lld/MachO/SectionPriorities.cpp
@@ -381,7 +381,8 @@ macho::PriorityBuilder::buildInputSectionPriorities() {
         config->bpCompressionSortSpecs, config->bpFunctionOrderForCompression,
         config->bpDataOrderForCompression,
         config->bpCompressionSortStartupFunctions,
-        config->bpStartupSortInitializers, config->bpVerboseSectionOrderer);
+        config->bpStartupSortInitializers, config->bpStartupSortObjCLoadMethods,
+        config->bpVerboseSectionOrderer);
   } else if (config->callGraphProfileSort) {
     // Sort sections by the profile data provided by __LLVM,__cg_profile
     // sections.
diff --git a/lld/include/lld/Common/BPSectionOrdererBase.inc b/lld/include/lld/Common/BPSectionOrdererBase.inc
index 53f45dd7fbb75..7231e2c1db5cc 100644
--- a/lld/include/lld/Common/BPSectionOrdererBase.inc
+++ b/lld/include/lld/Common/BPSectionOrdererBase.inc
@@ -69,7 +69,8 @@ template <class D> struct BPOrderer {
                     llvm::ArrayRef<Section *> sections,
                     const DenseMap<CachedHashStringRef, std::set<unsigned>>
                         &rootSymbolToSectionIdxs,
-                    llvm::ArrayRef<unsigned> initialStartupSectionIdxs)
+                    llvm::ArrayRef<unsigned> initialStartupSectionIdxs,
+                    llvm::ArrayRef<unsigned> prefixStartupSectionIdxs)
       -> llvm::DenseMap<const Section *, int>;
 
   std::optional<StringRef> static getResolvedLinkageName(StringRef name) {
@@ -166,7 +167,8 @@ auto BPOrderer<D>::computeOrder(
     ArrayRef<Section *> sections,
     const DenseMap<CachedHashStringRef, std::set<unsigned>>
         &rootSymbolToSectionIdxs,
-    ArrayRef<unsigned> initialStartupSectionIdxs)
+    ArrayRef<unsigned> initialStartupSectionIdxs,
+    ArrayRef<unsigned> prefixStartupSectionIdxs)
     -> DenseMap<const Section *, int> {
   TimeTraceScope timeScope("Setup Balanced Partitioning");
   DenseMap<const void *, uint64_t> sectionToIdx;
@@ -385,6 +387,13 @@ auto BPOrderer<D>::computeOrder(
   unsigned numCompressionSections = 0, compressionSize = 0;
   unsigned numDuplicateCompressionSections = 0, duplicateCompressionSize = 0;
   SetVector<const Section *> orderedSections;
+  for (unsigned sectionIdx : prefixStartupSectionIdxs) {
+    const auto *isec = sections[sectionIdx];
+    if (orderedSections.insert(isec)) {
+      startupSize += D::getSize(*isec);
+      ++numStartupSections;
+    }
+  }
   // Order startup functions,
   for (auto &node : nodesForStartup) {
     const auto *isec = sections[node.Id];
diff --git a/lld/test/MachO/bp-startup-sort-objc-load-methods.s b/lld/test/MachO/bp-startup-sort-objc-load-methods.s
new file mode 100644
index 0000000000000..2c202f3b7ce32
--- /dev/null
+++ b/lld/test/MachO/bp-startup-sort-objc-load-methods.s
@@ -0,0 +1,162 @@
+# REQUIRES: aarch64
+
+# RUN: rm -rf %t && split-file %s %t
+# RUN: llvm-mc -filetype=obj -triple=arm64-apple-macos11.0 %t/input.s -o %t/input.o
+# RUN: llvm-profdata merge %t/profile.proftext -o %t/profile.profdata
+# RUN: not %lld -arch arm64 -lSystem -e _main -o /dev/null %t/input.o --irpgo-profile=%t/profile.profdata --bp-startup-sort-objc-load-methods 2>&1 | FileCheck %s --check-prefix=ERROR
+# RUN: %lld -arch arm64 -lSystem -e _main -o %t/out %t/input.o --irpgo-profile=%t/profile.profdata --bp-startup-sort=function --bp-startup-sort-objc-load-methods --verbose-bp-section-orderer 2> %t/verbose
+# RUN: FileCheck %s --input-file=%t/verbose --check-prefix=VERBOSE
+# RUN: llvm-nm --numeric-sort --format=just-symbols %t/out | FileCheck %s --check-prefix=ORDER
+
+# ERROR: --bp-startup-sort-objc-load-methods must be used with --bp-startup-sort=function
+# VERBOSE: Objective-C +load functions for startup: 2
+# VERBOSE: Functions for startup: 3
+# ORDER: _class_load
+# ORDER-NEXT: _category_load
+# ORDER-NEXT: _profile_hot
+# ORDER-NEXT: _main
+
+#--- input.s
+ .subsections_via_symbols
+
+ .text
+ .globl _main
+_main:
+  ret
+
+ .globl _profile_hot
+_profile_hot:
+  mov w0, #1
+  ret
+
+ .globl _class_load
+_class_load:
+  mov w0, #2
+  ret
+
+ .globl _category_load
+_category_load:
+  mov w0, #3
+  ret
+
+ .section __TEXT,__objc_methname,cstring_literals
+L_load_name:
+  .asciz "load"
+
+ .section __TEXT,__objc_methtype,cstring_literals
+L_method_type:
+  .asciz "v16 at 0:8"
+
+ .section __TEXT,__objc_classname,cstring_literals
+L_class_name:
+  .asciz "Foo"
+L_category_name:
+  .asciz "Category"
+
+ .section __DATA,__objc_data
+ .p2align 3
+L_class_container:
+  .space 8
+  .quad L_metaclass_container + 8
+  .quad 0
+  .quad 0
+  .quad 0
+  .quad L_class_ro_container + 8
+
+L_metaclass_container:
+  .space 8
+  .quad L_metaclass_container + 8
+  .quad L_class_container + 8
+  .quad 0
+  .quad 0
+  .quad L_metaclass_ro_container + 8
+
+ .section __DATA,__objc_const
+ .p2align 3
+L_class_ro_container:
+  .space 8
+  .long 2
+  .long 0
+  .long 0
+  .space 4
+  .quad 0
+  .quad L_class_name
+  .quad 0
+  .quad 0
+  .quad 0
+  .quad 0
+  .quad 0
+
+L_metaclass_ro_container:
+  .space 8
+  .long 3
+  .long 40
+  .long 40
+  .space 4
+  .quad 0
+  .quad L_class_name
+  .quad L_class_methods_container + 8
+  .quad 0
+  .quad 0
+  .quad 0
+  .quad 0
+
+L_class_methods_container:
+  .space 8
+  .long 24
+  .long 1
+  .quad L_load_name
+  .quad L_method_type
+  .quad _class_load
+
+L_category_container:
+  .space 8
+  .quad L_category_name
+  .quad 0
+  .quad 0
+  .quad L_category_methods_container + 8
+  .quad 0
+  .quad 0
+  .quad 0
+  .long 64
+  .space 4
+
+L_category_methods_container:
+  .space 8
+  .long 0x8000000c
+  .long 1
+L_category_method_name:
+  .long L_load_name - L_category_method_name
+L_category_method_type:
+  .long L_method_type - L_category_method_type
+L_category_method_impl:
+  .long _category_load - L_category_method_impl
+
+ .section __DATA,__objc_nlclslist,regular,no_dead_strip
+  .quad L_class_container + 8
+
+ .section __DATA,__objc_nlcatlist,regular,no_dead_strip
+  .quad L_category_container + 8
+
+ .section __DATA,__objc_imageinfo,regular,no_dead_strip
+  .long 0
+  .long 64
+
+#--- profile.proftext
+:ir
+:temporal_prof_traces
+# Num Traces
+1
+# Trace Stream Size:
+1
+# Weight
+1
+profile_hot
+
+profile_hot
+# Func Hash:
+1111
+# Num Counters:
+1
+# Counter Values:
+1

>From a73a645fdddfa8534f17d3756119571d3f729789 Mon Sep 17 00:00:00 2001
From: Karim Alweheshy <karim.alweheshy at gmail.com>
Date: Wed, 5 Aug 2026 07:34:26 +0200
Subject: [PATCH 3/3] [lld][MachO] Sort BP section orderer includes

---
 lld/MachO/BPSectionOrderer.cpp | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/lld/MachO/BPSectionOrderer.cpp b/lld/MachO/BPSectionOrderer.cpp
index a3daeec9b9b70..32ead5ed21251 100644
--- a/lld/MachO/BPSectionOrderer.cpp
+++ b/lld/MachO/BPSectionOrderer.cpp
@@ -12,8 +12,8 @@
 #include "OutputSegment.h"
 #include "Relocations.h"
 #include "Symbols.h"
-#include "Target.h"
 #include "SyntheticSections.h"
+#include "Target.h"
 #include "lld/Common/BPSectionOrdererBase.inc"
 #include "llvm/ADT/DenseMap.h"
 #include "llvm/ADT/STLExtras.h"



More information about the llvm-commits mailing list