[llvm] [AArch64] Use a frame record for non-leaf outlined functions on MachO (PR #213711)

via llvm-commits llvm-commits at lists.llvm.org
Wed Aug 5 08:23:16 PDT 2026


llvmorg-github-actions[bot] wrote:


<!--LLVM PR SUMMARY COMMENT-->

@llvm/pr-subscribers-backend-aarch64

Author: Kyungwoo Lee (kyulee-com)

<details>
<summary>Changes</summary>

A non-leaf outlined function saves LR. Saving it alone (str x30) can't be described by MachO compact unwind, so the encoder falls back to a larger DWARF FDE. Saving a full frame record (stp x29,x30 ; mov x29,sp) with the matching CFI gets the small UNWIND_ARM64_MODE_FRAME encoding instead, for one extra instruction (the mov). getOutliningCandidateInfo accounts for it.

Only on MachO, where the ABI keeps x29 reserved as the frame pointer, so setting it up here can't clobber a live value. Other targets keep the str x30 save (DWARF describes it fine).

Size on a large iOS binary (~70 MiB of code): ~630 KiB smaller uncompressed -- ~370 KiB less unwind info (compact FRAME instead of DWARF FDEs) and ~260 KiB less .text. The .text win is a link-time effect: with compact unwind the linker can ICF-fold identical outlined functions; a per-function DWARF FDE (which points at its own function) keeps them distinct and unfoldable.

Assisted-by: Claude Opus 4.8

---
Full diff: https://github.com/llvm/llvm-project/pull/213711.diff


3 Files Affected:

- (modified) llvm/lib/Target/AArch64/AArch64InstrInfo.cpp (+125-23) 
- (added) llvm/test/CodeGen/AArch64/machine-outliner-macho-compact-unwind-frame-fp.mir (+89) 
- (added) llvm/test/CodeGen/AArch64/machine-outliner-macho-compact-unwind-frame.mir (+107) 


``````````diff
diff --git a/llvm/lib/Target/AArch64/AArch64InstrInfo.cpp b/llvm/lib/Target/AArch64/AArch64InstrInfo.cpp
index ac827debe207c..6c7610209ba2a 100644
--- a/llvm/lib/Target/AArch64/AArch64InstrInfo.cpp
+++ b/llvm/lib/Target/AArch64/AArch64InstrInfo.cpp
@@ -27,6 +27,7 @@
 #include "llvm/Analysis/AliasAnalysis.h"
 #include "llvm/CodeGen/CFIInstBuilder.h"
 #include "llvm/CodeGen/LivePhysRegs.h"
+#include "llvm/CodeGen/LiveRegUnits.h"
 #include "llvm/CodeGen/MachineBasicBlock.h"
 #include "llvm/CodeGen/MachineCombinerPattern.h"
 #include "llvm/CodeGen/MachineFrameInfo.h"
@@ -103,6 +104,10 @@ static cl::opt<unsigned> GatherOptSearchLimit(
     cl::desc("Restrict range of instructions to search for the "
              "machine-combiner gather pattern optimization"));
 
+static cl::opt<bool> UseCompactUnwindFrameRecordForOutlinedFunctions(
+    "aarch64-outliner-compact-unwind-frame", cl::Hidden, cl::init(true),
+    cl::desc("Use a frame record for Mach-O non-leaf outlined functions"));
+
 AArch64InstrInfo::AArch64InstrInfo(const AArch64Subtarget &STI)
     : AArch64GenInstrInfo(STI, RI, AArch64::ADJCALLSTACKDOWN,
                           AArch64::ADJCALLSTACKUP, AArch64::CATCHRET),
@@ -10283,6 +10288,32 @@ enum MachineOutlinerMBBFlags {
   UnsafeRegsDead = 0x8
 };
 
+/// Return true when a non-leaf outlined function should save FP and LR as a
+/// frame record instead of saving LR alone.
+///
+/// This is only useful for MachO functions that need unwind info. Do not use
+/// this form if the outlined sequence reads or writes x29. The prologue sets
+/// x29 before the outlined body runs, and the epilogue restores the old x29
+/// before returning.
+static bool shouldUseCompactUnwindFrameRecordForOutlinedFunction(
+    const MachineFunction &MF, bool NeedsDwarf, bool FPAvailableInsideSeq) {
+  return UseCompactUnwindFrameRecordForOutlinedFunctions && NeedsDwarf &&
+         FPAvailableInsideSeq &&
+         MF.getTarget().getTargetTriple().isOSBinFormatMachO();
+}
+
+/// Return true if no instruction in \p MBB reads, writes or clobbers FP.
+///
+/// Use the same LiveRegUnits query as Candidate::isAvailableInsideSeq(), so the
+/// cost estimate and the emission code make the same decision.
+static bool isFPAvailableInsideOutlinedSeq(const MachineBasicBlock &MBB,
+                                           const TargetRegisterInfo &TRI) {
+  LiveRegUnits LRU(TRI);
+  for (const MachineInstr &MI : MBB.instrs())
+    LRU.accumulate(MI);
+  return LRU.available(AArch64::FP);
+}
+
 Register
 AArch64InstrInfo::findRegisterToSaveLRTo(outliner::Candidate &C) const {
   MachineFunction *MF = C.getMF();
@@ -10749,6 +10780,29 @@ AArch64InstrInfo::getOutliningCandidateInfo(
 
       // Save + restore LR.
       NumBytesToCreateFrame += 8;
+
+      bool FPAvailableInsideSeq =
+          llvm::all_of(RepeatedSequenceLocs, [&TRI](outliner::Candidate &C) {
+            return C.isAvailableInsideSeq(AArch64::FP, TRI);
+          });
+      // The outlined function does not exist yet, so use the candidates to
+      // predict whether it will need unwind info. The generic outliner uses
+      // the strongest UWTableKind from the candidates, and keeps nounwind only
+      // when every candidate is nounwind.
+      bool NeedsDwarfUnwind =
+          llvm::any_of(RepeatedSequenceLocs, [](outliner::Candidate &C) {
+            const MachineFunction &MF = *C.getMF();
+            return MF.getInfo<AArch64FunctionInfo>()->needsDwarfUnwindInfo(MF);
+          });
+      bool UseFrameRecord =
+          shouldUseCompactUnwindFrameRecordForOutlinedFunction(
+              *RepeatedSequenceLocs.front().getMF(), NeedsDwarfUnwind,
+              FPAvailableInsideSeq);
+
+      // Account for the extra mov only when buildOutlinedFrame will use the
+      // MachO frame-record form.
+      if (UseFrameRecord)
+        NumBytesToCreateFrame += 4;
     }
   }
 
@@ -11181,32 +11235,80 @@ void AArch64InstrInfo::buildOutlinedFrame(
         OF.FrameConstructionID == MachineOutlinerThunk)
       Et = std::prev(MBB.end());
 
-    // Insert a save before the outlined region
-    MachineInstr *STRXpre = BuildMI(MF, DebugLoc(), get(AArch64::STRXpre))
-                                .addReg(AArch64::SP, RegState::Define)
-                                .addReg(AArch64::LR)
+    // There is a call in the range, so we must save LR. On MachO, saving only
+    // LR cannot be described by compact unwind. Saving FP and LR as a frame
+    // record lets the compact-unwind encoder use the FRAME encoding, at the
+    // cost of one extra mov.
+    bool NeedsDwarf =
+        MF.getInfo<AArch64FunctionInfo>()->needsDwarfUnwindInfo(MF);
+    bool UseFrameRecord = shouldUseCompactUnwindFrameRecordForOutlinedFunction(
+        MF, NeedsDwarf, isFPAvailableInsideOutlinedSeq(MBB, getRegisterInfo()));
+
+    if (UseFrameRecord) {
+      // FP is saved here, so it must be live-in.
+      if (!MBB.isLiveIn(AArch64::FP))
+        MBB.addLiveIn(AArch64::FP);
+
+      // stp x29, x30, [sp, #-16]!   (STP pre-index imm is scaled by 8: -2 * 8)
+      MachineInstr *STPXpre = BuildMI(MF, DebugLoc(), get(AArch64::STPXpre))
+                                  .addReg(AArch64::SP, RegState::Define)
+                                  .addReg(AArch64::FP)
+                                  .addReg(AArch64::LR)
+                                  .addReg(AArch64::SP)
+                                  .addImm(-2);
+      It = MBB.insert(It, STPXpre);
+
+      // mov x29, sp  (add x29, sp, #0) so x29 points at the saved frame record.
+      MachineInstr *SetFP = BuildMI(MF, DebugLoc(), get(AArch64::ADDXri))
+                                .addReg(AArch64::FP, RegState::Define)
                                 .addReg(AArch64::SP)
-                                .addImm(-16);
-    It = MBB.insert(It, STRXpre);
-
-    if (MF.getInfo<AArch64FunctionInfo>()->needsDwarfUnwindInfo(MF)) {
-      CFIInstBuilder CFIBuilder(MBB, It, MachineInstr::FrameSetup);
-
-      // Add a CFI saying the stack was moved 16 B down.
-      CFIBuilder.buildDefCFAOffset(16);
+                                .addImm(0)
+                                .addImm(0);
+      MBB.insertAfter(It, SetFP);
+
+      // Standard frame-record CFI (FP is the CFA) so the encoder emits FRAME.
+      CFIInstBuilder CFIBuilder(MBB, std::next(SetFP->getIterator()),
+                                MachineInstr::FrameSetup);
+      CFIBuilder.buildDefCFA(AArch64::FP, 16);
+      CFIBuilder.buildOffset(AArch64::LR, -8);
+      CFIBuilder.buildOffset(AArch64::FP, -16);
+
+      // ldp x29, x30, [sp], #16
+      MachineInstr *LDPXpost = BuildMI(MF, DebugLoc(), get(AArch64::LDPXpost))
+                                   .addReg(AArch64::SP, RegState::Define)
+                                   .addReg(AArch64::FP, RegState::Define)
+                                   .addReg(AArch64::LR, RegState::Define)
+                                   .addReg(AArch64::SP)
+                                   .addImm(2);
+      Et = MBB.insert(Et, LDPXpost);
+    } else {
+      // Insert a save before the outlined region
+      MachineInstr *STRXpre = BuildMI(MF, DebugLoc(), get(AArch64::STRXpre))
+                                  .addReg(AArch64::SP, RegState::Define)
+                                  .addReg(AArch64::LR)
+                                  .addReg(AArch64::SP)
+                                  .addImm(-16);
+      It = MBB.insert(It, STRXpre);
+
+      if (NeedsDwarf) {
+        CFIInstBuilder CFIBuilder(MBB, It, MachineInstr::FrameSetup);
+
+        // Add a CFI saying the stack was moved 16 B down.
+        CFIBuilder.buildDefCFAOffset(16);
+
+        // Add a CFI saying that the LR that we want to find is now 16 B higher
+        // than before.
+        CFIBuilder.buildOffset(AArch64::LR, -16);
+      }
 
-      // Add a CFI saying that the LR that we want to find is now 16 B higher
-      // than before.
-      CFIBuilder.buildOffset(AArch64::LR, -16);
+      // Insert a restore before the terminator for the function.
+      MachineInstr *LDRXpost = BuildMI(MF, DebugLoc(), get(AArch64::LDRXpost))
+                                   .addReg(AArch64::SP, RegState::Define)
+                                   .addReg(AArch64::LR, RegState::Define)
+                                   .addReg(AArch64::SP)
+                                   .addImm(16);
+      Et = MBB.insert(Et, LDRXpost);
     }
-
-    // Insert a restore before the terminator for the function.
-    MachineInstr *LDRXpost = BuildMI(MF, DebugLoc(), get(AArch64::LDRXpost))
-                                 .addReg(AArch64::SP, RegState::Define)
-                                 .addReg(AArch64::LR, RegState::Define)
-                                 .addReg(AArch64::SP)
-                                 .addImm(16);
-    Et = MBB.insert(Et, LDRXpost);
   }
 
   auto RASignCondition = FI->getSignReturnAddressCondition();
diff --git a/llvm/test/CodeGen/AArch64/machine-outliner-macho-compact-unwind-frame-fp.mir b/llvm/test/CodeGen/AArch64/machine-outliner-macho-compact-unwind-frame-fp.mir
new file mode 100644
index 0000000000000..4187c6ad308e5
--- /dev/null
+++ b/llvm/test/CodeGen/AArch64/machine-outliner-macho-compact-unwind-frame-fp.mir
@@ -0,0 +1,89 @@
+# RUN: llc -mtriple=arm64-apple-darwin -run-pass=prolog-epilog -run-pass=machine-outliner \
+# RUN:     -verify-machineinstrs %s -o - | FileCheck %s
+
+# Do not use the MachO compact-unwind frame-record form when the outlined
+# sequence itself reads or writes FP. Restoring FP in the outlined epilogue
+# would clobber the sequence's result.
+
+--- |
+  define void @baz() #0 { ret void }
+  define void @bar(i32 %a) #0 { ret void }
+  attributes #0 = { noredzone uwtable }
+...
+---
+name:            bar
+tracksRegLiveness: true
+body:             |
+  bb.0:
+    liveins: $w0, $lr, $fp
+    $sp = frame-setup SUBXri $sp, 32, 0
+    $fp = frame-setup ADDXri $sp, 16, 0
+
+  bb.1:
+    BL @baz, implicit-def dead $lr, implicit $sp
+    $x9 = ORRXrs $xzr, $fp, 0
+    $fp = ORRXrs $xzr, $x9, 0
+    BL @baz, implicit-def dead $lr, implicit $sp
+    $w17 = ORRWri $wzr, 1
+    $w0 = ORRWri $wzr, 6
+
+    BL @baz, implicit-def dead $lr, implicit $sp
+    $x9 = ORRXrs $xzr, $fp, 0
+    $fp = ORRXrs $xzr, $x9, 0
+    BL @baz, implicit-def dead $lr, implicit $sp
+    $w17 = ORRWri $wzr, 1
+    $w0 = ORRWri $wzr, 5
+
+    BL @baz, implicit-def dead $lr, implicit $sp
+    $x9 = ORRXrs $xzr, $fp, 0
+    $fp = ORRXrs $xzr, $x9, 0
+    BL @baz, implicit-def dead $lr, implicit $sp
+    $w17 = ORRWri $wzr, 1
+    $w0 = ORRWri $wzr, 4
+
+    BL @baz, implicit-def dead $lr, implicit $sp
+    $x9 = ORRXrs $xzr, $fp, 0
+    $fp = ORRXrs $xzr, $x9, 0
+    BL @baz, implicit-def dead $lr, implicit $sp
+    $w17 = ORRWri $wzr, 1
+    $w0 = ORRWri $wzr, 3
+
+    BL @baz, implicit-def dead $lr, implicit $sp
+    $x9 = ORRXrs $xzr, $fp, 0
+    $fp = ORRXrs $xzr, $x9, 0
+    BL @baz, implicit-def dead $lr, implicit $sp
+    $w17 = ORRWri $wzr, 1
+    $w0 = ORRWri $wzr, 2
+
+    BL @baz, implicit-def dead $lr, implicit $sp
+    $x9 = ORRXrs $xzr, $fp, 0
+    $fp = ORRXrs $xzr, $x9, 0
+    BL @baz, implicit-def dead $lr, implicit $sp
+    $w17 = ORRWri $wzr, 1
+    $w0 = ORRWri $wzr, 1
+
+  bb.2:
+    $fp, $lr = LDPXi $sp, 2
+    RET undef $lr
+...
+---
+name:            baz
+tracksRegLiveness: true
+body:             |
+  bb.0:
+    liveins: $w0, $lr
+    RET undef $lr
+
+# CHECK: name:            OUTLINED_FUNCTION_0
+# CHECK-NOT: STPXpre
+# CHECK-NOT: $fp = ADDXri $sp, 0, 0
+# CHECK: frame-setup CFI_INSTRUCTION def_cfa_offset 16
+# CHECK-NEXT: frame-setup CFI_INSTRUCTION offset $w30, -16
+# CHECK-NEXT: early-clobber $sp = STRXpre $lr, $sp, -16
+# CHECK-NEXT: BL @baz, implicit-def dead $lr, implicit $sp
+# CHECK-NEXT: $x9 = ORRXrs $xzr, $fp, 0
+# CHECK-NEXT: $fp = ORRXrs $xzr, $x9, 0
+# CHECK-NEXT: BL @baz, implicit-def dead $lr, implicit $sp
+# CHECK-NEXT: $w17 = ORRWri $wzr, 1
+# CHECK: early-clobber $sp, $lr = LDRXpost $sp, 16
+# CHECK-NEXT: RET $lr
diff --git a/llvm/test/CodeGen/AArch64/machine-outliner-macho-compact-unwind-frame.mir b/llvm/test/CodeGen/AArch64/machine-outliner-macho-compact-unwind-frame.mir
new file mode 100644
index 0000000000000..2cfc9b12c8f7b
--- /dev/null
+++ b/llvm/test/CodeGen/AArch64/machine-outliner-macho-compact-unwind-frame.mir
@@ -0,0 +1,107 @@
+# RUN: llc -mtriple=arm64-apple-darwin -run-pass=prolog-epilog -run-pass=machine-outliner \
+# RUN:     -verify-machineinstrs %s -o - | FileCheck %s --check-prefix=MACHO
+# RUN: llc -mtriple=arm64-apple-darwin -run-pass=prolog-epilog -run-pass=machine-outliner \
+# RUN:     -aarch64-outliner-compact-unwind-frame=false -verify-machineinstrs %s -o - | \
+# RUN:     FileCheck %s --check-prefix=NOFRAME
+# RUN: llc -mtriple=aarch64-linux-gnu -run-pass=prolog-epilog -run-pass=machine-outliner \
+# RUN:     -verify-machineinstrs %s -o - | FileCheck %s --check-prefix=ELF
+
+# A non-leaf outlined function must save LR. On MachO, save it as a full frame
+# record (stp x29,x30 ; mov x29,sp) so the compact-unwind encoder can use the
+# small FRAME encoding instead of falling back to a DWARF FDE. Everywhere else
+# keep the cheaper LR-alone save (str x30), which is all DWARF unwind needs.
+
+--- |
+  define void @baz() #0 { ret void }
+  define void @bar(i32 %a) #0 { ret void }
+  attributes #0 = { noredzone uwtable }
+...
+---
+name:            bar
+tracksRegLiveness: true
+body:             |
+  bb.0:
+    liveins: $w0, $lr, $w8
+    $sp = frame-setup SUBXri $sp, 32, 0
+    $fp = frame-setup ADDXri $sp, 16, 0
+
+  bb.1:
+    ; Calls interleaved with single defs so the only repeated sequence is the
+    ; call-containing one, forcing a MachineOutlinerDefault (LR-save) frame.
+    BL @baz, implicit-def dead $lr, implicit $sp
+    $w17 = ORRWri $wzr, 1
+    BL @baz, implicit-def dead $lr, implicit $sp
+    $w17 = ORRWri $wzr, 1
+    $w0 = ORRWri $wzr, 6
+
+    BL @baz, implicit-def dead $lr, implicit $sp
+    $w17 = ORRWri $wzr, 1
+    BL @baz, implicit-def dead $lr, implicit $sp
+    $w17 = ORRWri $wzr, 1
+    $w0 = ORRWri $wzr, 5
+
+    BL @baz, implicit-def dead $lr, implicit $sp
+    $w17 = ORRWri $wzr, 1
+    BL @baz, implicit-def dead $lr, implicit $sp
+    $w17 = ORRWri $wzr, 1
+    $w0 = ORRWri $wzr, 4
+
+    BL @baz, implicit-def dead $lr, implicit $sp
+    $w17 = ORRWri $wzr, 1
+    BL @baz, implicit-def dead $lr, implicit $sp
+    $w17 = ORRWri $wzr, 1
+    $w0 = ORRWri $wzr, 3
+
+    BL @baz, implicit-def dead $lr, implicit $sp
+    $w17 = ORRWri $wzr, 1
+    BL @baz, implicit-def dead $lr, implicit $sp
+    $w17 = ORRWri $wzr, 1
+    $w0 = ORRWri $wzr, 2
+
+    BL @baz, implicit-def dead $lr, implicit $sp
+    $w17 = ORRWri $wzr, 1
+    BL @baz, implicit-def dead $lr, implicit $sp
+    $w17 = ORRWri $wzr, 1
+    $w0 = ORRWri $wzr, 1
+
+  bb.2:
+    $fp, $lr = LDPXi $sp, 2
+    RET undef $lr
+...
+---
+name:            baz
+tracksRegLiveness: true
+body:             |
+  bb.0:
+    liveins: $w0, $lr, $w8
+    RET undef $lr
+
+# MachO: full frame record + canonical CFI -> compact unwind FRAME encoding.
+# MACHO: name:            OUTLINED_FUNCTION_0
+# MACHO: early-clobber $sp = STPXpre $fp, $lr, $sp, -2
+# MACHO-NEXT: $fp = ADDXri $sp, 0, 0
+# MACHO-NEXT: frame-setup CFI_INSTRUCTION def_cfa $w29, 16
+# MACHO-NEXT: frame-setup CFI_INSTRUCTION offset $w30, -8
+# MACHO-NEXT: frame-setup CFI_INSTRUCTION offset $w29, -16
+# MACHO-NEXT: BL @baz, implicit-def dead $lr, implicit $sp
+# MACHO: early-clobber $sp, $fp, $lr = LDPXpost $sp, 2
+# MACHO-NEXT: RET $lr
+
+# With the frame-record optimization disabled, MachO keeps the LR-alone save.
+# NOFRAME: name:            OUTLINED_FUNCTION_0
+# NOFRAME-NOT: STPXpre
+# NOFRAME: frame-setup CFI_INSTRUCTION def_cfa_offset 16
+# NOFRAME-NEXT: frame-setup CFI_INSTRUCTION offset $w30, -16
+# NOFRAME-NEXT: early-clobber $sp = STRXpre $lr, $sp, -16
+# NOFRAME-NEXT: BL @baz, implicit-def dead $lr, implicit $sp
+# NOFRAME: early-clobber $sp, $lr = LDRXpost $sp, 16
+# NOFRAME-NEXT: RET $lr
+
+# On ELF the LR-alone save is unchanged.
+# ELF: name:            OUTLINED_FUNCTION_0
+# ELF: frame-setup CFI_INSTRUCTION def_cfa_offset 16
+# ELF-NEXT: frame-setup CFI_INSTRUCTION offset $w30, -16
+# ELF-NEXT: early-clobber $sp = STRXpre $lr, $sp, -16
+# ELF-NEXT: BL @baz, implicit-def dead $lr, implicit $sp
+# ELF: early-clobber $sp, $lr = LDRXpost $sp, 16
+# ELF-NEXT: RET $lr

``````````

</details>


https://github.com/llvm/llvm-project/pull/213711


More information about the llvm-commits mailing list