[lld] [lld][ELF] Add --print-relax-stats= to emit linker relaxation statistics (PR #193631)

Farid Zakaria via llvm-commits llvm-commits at lists.llvm.org
Wed Apr 22 17:48:29 PDT 2026


https://github.com/fzakaria updated https://github.com/llvm/llvm-project/pull/193631

>From ef20246ea1da45498d562fecfa6a09ebbe561e07 Mon Sep 17 00:00:00 2001
From: Farid Zakaria <fmzakari at fb.com>
Date: Wed, 22 Apr 2026 17:08:46 -0700
Subject: [PATCH 1/2] [lld][ELF] Add --print-relax-stats= to emit linker
 relaxation statistics
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Add a new flag --print-relax-stats=<file> that writes per-relocation-type
relaxation statistics to a JSON file, following the pattern of
--print-archive-stats=.

This is motivated by ongoing work on large code model relaxations
(e.g. movabs→lea, movabs+call→call), where relaxation is not guaranteed
since displacements may not fit in 32 bits. Knowing the relaxation
success rate is critical for evaluating effectiveness. The flag is also
useful in the small code model to understand how many GOT indirections
or TLS accesses remain unoptimized (e.g. due to symbol preemptibility).

Currently tracks x86_64 GOT and TLS relaxation types. The infrastructure
is architecture-agnostic (array indexed by RelType, names derived from
the ELF relocation enum via toStr), so other targets can add tracking by
implementing their own trackRelaxStats in relocateAlloc.

Counters use std::atomic since relocateAlloc runs sections in parallel.
The stats array is only allocated when the flag is specified.

Sample output:

  {
    "relaxations": {
      "R_X86_64_REX_GOTPCRELX": {
        "total": 2,
        "relaxed": 1,
        "pct_relaxed": 50
      },
      "R_X86_64_TLSGD": {
        "total": 1,
        "relaxed": 1,
        "pct_relaxed": 100
      }
    }
  }
---
 lld/ELF/Arch/X86_64.cpp          | 32 ++++++++++++++++++++++++
 lld/ELF/Config.h                 |  8 ++++++
 lld/ELF/Driver.cpp               | 39 +++++++++++++++++++++++++++++
 lld/ELF/Options.td               |  3 +++
 lld/test/ELF/print-relax-stats.s | 42 ++++++++++++++++++++++++++++++++
 5 files changed, 124 insertions(+)
 create mode 100644 lld/test/ELF/print-relax-stats.s

diff --git a/lld/ELF/Arch/X86_64.cpp b/lld/ELF/Arch/X86_64.cpp
index 1447967e48171..89456520acb49 100644
--- a/lld/ELF/Arch/X86_64.cpp
+++ b/lld/ELF/Arch/X86_64.cpp
@@ -1251,6 +1251,36 @@ bool X86_64::adjustPrologueForCrossSplitStack(uint8_t *loc, uint8_t *end,
   return false;
 }
 
+// rel.expr was set during scanRelocations to encode whether relaxation will be
+// applied (e.g. R_RELAX_GOT_PC vs R_GOT_PC). We reuse those same checks here.
+static void trackRelaxStats(Ctx &ctx, const Relocation &rel) {
+  bool relaxed;
+  switch (rel.type) {
+  case R_X86_64_GOTPCRELX:
+  case R_X86_64_REX_GOTPCRELX:
+  case R_X86_64_CODE_4_GOTPCRELX:
+    relaxed = rel.expr != R_GOT_PC;
+    break;
+  case R_X86_64_GOTPC32_TLSDESC:
+  case R_X86_64_CODE_4_GOTPC32_TLSDESC:
+  case R_X86_64_TLSGD:
+    relaxed = rel.expr == R_TPREL || rel.expr == R_GOT_PC;
+    break;
+  case R_X86_64_TLSLD:
+  case R_X86_64_GOTTPOFF:
+  case R_X86_64_CODE_4_GOTTPOFF:
+  case R_X86_64_CODE_6_GOTTPOFF:
+    relaxed = rel.expr == R_TPREL;
+    break;
+  default:
+    return;
+  }
+  auto &entry = ctx.relaxStats[rel.type];
+  entry.total.fetch_add(1, std::memory_order_relaxed);
+  if (relaxed)
+    entry.relaxed.fetch_add(1, std::memory_order_relaxed);
+}
+
 void X86_64::relocateAlloc(InputSection &sec, uint8_t *buf) const {
   uint64_t secAddr = sec.getOutputSection()->addr + sec.outSecOff;
   for (const Relocation &rel : sec.relocs()) {
@@ -1259,6 +1289,8 @@ void X86_64::relocateAlloc(InputSection &sec, uint8_t *buf) const {
     uint8_t *loc = buf + rel.offset;
     const uint64_t val = sec.getRelocTargetVA(ctx, rel, secAddr + rel.offset);
     relocate(loc, rel, val);
+    if (!ctx.arg.printRelaxStats.empty())
+      trackRelaxStats(ctx, rel);
   }
   if (sec.jumpInstrMod) {
     applyJumpInstrMod(buf + sec.jumpInstrMod->offset,
diff --git a/lld/ELF/Config.h b/lld/ELF/Config.h
index cf3cde1cf2696..54a9be3412f8f 100644
--- a/lld/ELF/Config.h
+++ b/lld/ELF/Config.h
@@ -241,6 +241,7 @@ struct Config {
   llvm::StringRef optStatsFilename;
   llvm::StringRef progName;
   llvm::StringRef printArchiveStats;
+  llvm::StringRef printRelaxStats;
   llvm::StringRef printSymbolOrder;
   llvm::StringRef soName;
   llvm::StringRef sysroot;
@@ -735,6 +736,13 @@ struct Ctx : CommonLinkerContext {
   llvm::raw_fd_ostream openAuxiliaryFile(llvm::StringRef, std::error_code &);
 
   std::optional<AArch64PauthAbiCoreInfo> aarch64PauthAbiCoreInfo;
+
+  struct RelaxStatEntry {
+    std::atomic<uint64_t> total{0};
+    std::atomic<uint64_t> relaxed{0};
+  };
+  static constexpr unsigned relaxStatsMaxType = 2048;
+  std::unique_ptr<RelaxStatEntry[]> relaxStats;
 };
 
 // The first two elements of versionDefinitions represent VER_NDX_LOCAL and
diff --git a/lld/ELF/Driver.cpp b/lld/ELF/Driver.cpp
index 24718d2d2186a..facdcbcdcc2ae 100644
--- a/lld/ELF/Driver.cpp
+++ b/lld/ELF/Driver.cpp
@@ -57,6 +57,7 @@
 #include "llvm/Support/Compression.h"
 #include "llvm/Support/FileSystem.h"
 #include "llvm/Support/GlobPattern.h"
+#include "llvm/Support/JSON.h"
 #include "llvm/Support/LEB128.h"
 #include "llvm/Support/Parallel.h"
 #include "llvm/Support/Path.h"
@@ -1590,6 +1591,9 @@ static void readConfigs(Ctx &ctx, opt::InputArgList &args) {
   }
   ctx.arg.printMemoryUsage = args.hasArg(OPT_print_memory_usage);
   ctx.arg.printArchiveStats = args.getLastArgValue(OPT_print_archive_stats);
+  ctx.arg.printRelaxStats = args.getLastArgValue(OPT_print_relax_stats);
+  if (!ctx.arg.printRelaxStats.empty())
+    ctx.relaxStats.reset(new Ctx::RelaxStatEntry[Ctx::relaxStatsMaxType]());
   ctx.arg.printSymbolOrder = args.getLastArgValue(OPT_print_symbol_order);
   ctx.arg.rejectMismatch = !args.hasArg(OPT_no_warn_mismatch);
   ctx.arg.relax = args.hasFlag(OPT_relax, OPT_no_relax, true);
@@ -2491,6 +2495,39 @@ static void writeArchiveStats(Ctx &ctx) {
   }
 }
 
+static void writeRelaxStats(Ctx &ctx) {
+  if (ctx.arg.printRelaxStats.empty() || !ctx.relaxStats)
+    return;
+
+  std::error_code ec;
+  raw_fd_ostream os = ctx.openAuxiliaryFile(ctx.arg.printRelaxStats, ec);
+  if (ec) {
+    ErrAlways(ctx) << "--print-relax-stats=: cannot open "
+                   << ctx.arg.printRelaxStats << ": " << ec.message();
+    return;
+  }
+
+  llvm::json::OStream j(os, 2);
+  j.object([&] {
+    j.attributeObject("relaxations", [&] {
+      for (unsigned i = 0; i < Ctx::relaxStatsMaxType; ++i) {
+        uint64_t total =
+            ctx.relaxStats[i].total.load(std::memory_order_relaxed);
+        if (total == 0)
+          continue;
+        uint64_t relaxed =
+            ctx.relaxStats[i].relaxed.load(std::memory_order_relaxed);
+        j.attributeObject(toStr(ctx, RelType(i)), [&] {
+          j.attribute("total", int64_t(total));
+          j.attribute("relaxed", int64_t(relaxed));
+          j.attribute("pct_relaxed", relaxed * 100.0 / total);
+        });
+      }
+    });
+  });
+  os << '\n';
+}
+
 static void writeWhyExtract(Ctx &ctx) {
   if (ctx.arg.whyExtract.empty())
     return;
@@ -3605,4 +3642,6 @@ template <class ELFT> void LinkerDriver::link(opt::InputArgList &args) {
 
   // Write the result to the file.
   writeResult<ELFT>(ctx);
+
+  writeRelaxStats(ctx);
 }
diff --git a/lld/ELF/Options.td b/lld/ELF/Options.td
index 64c42eb49607d..a5261a3df7290 100644
--- a/lld/ELF/Options.td
+++ b/lld/ELF/Options.td
@@ -403,6 +403,9 @@ def print_archive_stats: J<"print-archive-stats=">,
   HelpText<"Write archive usage statistics to the specified file. "
            "Print the numbers of members and extracted members for each archive">;
 
+def print_relax_stats: J<"print-relax-stats=">,
+  HelpText<"Write linker relaxation statistics to the specified file as JSON">;
+
 defm print_symbol_order: Eq<"print-symbol-order",
   "Print a symbol order specified by --call-graph-ordering-file into the specified file">;
 
diff --git a/lld/test/ELF/print-relax-stats.s b/lld/test/ELF/print-relax-stats.s
new file mode 100644
index 0000000000000..3e9c5f5e80f59
--- /dev/null
+++ b/lld/test/ELF/print-relax-stats.s
@@ -0,0 +1,42 @@
+# REQUIRES: x86
+
+# RUN: rm -rf %t && split-file %s %t && cd %t
+# RUN: llvm-mc -filetype=obj -triple=x86_64 a.s -o a.o
+
+## Test that --print-relax-stats produces JSON with per-type relaxation counts.
+# RUN: ld.lld a.o -o a --print-relax-stats=stats.json
+# RUN: FileCheck %s --input-file=stats.json
+
+# CHECK:      "relaxations": {
+# CHECK-NEXT:   "R_X86_64_REX_GOTPCRELX": {
+# CHECK-NEXT:     "total": 1,
+# CHECK-NEXT:     "relaxed": 1,
+# CHECK:        }
+# CHECK:      }
+
+## --no-relax disables GOT optimization. Verify total > 0 but relaxed == 0.
+# RUN: ld.lld --no-relax a.o -o a2 --print-relax-stats=stats-norelax.json
+# RUN: FileCheck %s --check-prefix=NORELAX --input-file=stats-norelax.json
+# NORELAX:      "relaxations": {
+# NORELAX-NEXT:   "R_X86_64_REX_GOTPCRELX": {
+# NORELAX-NEXT:     "total": 1,
+# NORELAX-NEXT:     "relaxed": 0,
+# NORELAX:        }
+# NORELAX:      }
+
+## - means stdout.
+# RUN: ld.lld a.o -o a3 --print-relax-stats=- | FileCheck %s
+
+## Error opening file.
+# RUN: not ld.lld a.o -o /dev/null --print-relax-stats=/ 2>&1 | FileCheck --check-prefix=ERR %s
+# ERR: error: --print-relax-stats=: cannot open /: {{.*}}
+
+#--- a.s
+.globl _start, foo
+.hidden foo
+
+_start:
+  movq foo at GOTPCREL(%rip), %rax
+
+foo:
+  ret

>From 511607ca66e3ab1b363304b4fdaa38bf4cb3cc10 Mon Sep 17 00:00:00 2001
From: Farid Zakaria <fmzakari at fb.com>
Date: Wed, 22 Apr 2026 17:48:11 -0700
Subject: [PATCH 2/2] Refactor print-relax-stats

Remove lock and change to single-pass calculation at the end.
In order to do this, we need a relax bit on the struct to capture whether it
was relaxed.

We could have done this also via the rel.expr but I personally find that
brittle.
---
 lld/ELF/Arch/X86_64.cpp | 32 --------------------------------
 lld/ELF/Config.h        |  7 -------
 lld/ELF/Driver.cpp      | 33 ++++++++++++++++++++++++---------
 lld/ELF/RelocScan.h     | 12 ++++++------
 lld/ELF/Relocations.cpp |  3 ++-
 lld/ELF/Relocations.h   |  1 +
 6 files changed, 33 insertions(+), 55 deletions(-)

diff --git a/lld/ELF/Arch/X86_64.cpp b/lld/ELF/Arch/X86_64.cpp
index 89456520acb49..1447967e48171 100644
--- a/lld/ELF/Arch/X86_64.cpp
+++ b/lld/ELF/Arch/X86_64.cpp
@@ -1251,36 +1251,6 @@ bool X86_64::adjustPrologueForCrossSplitStack(uint8_t *loc, uint8_t *end,
   return false;
 }
 
-// rel.expr was set during scanRelocations to encode whether relaxation will be
-// applied (e.g. R_RELAX_GOT_PC vs R_GOT_PC). We reuse those same checks here.
-static void trackRelaxStats(Ctx &ctx, const Relocation &rel) {
-  bool relaxed;
-  switch (rel.type) {
-  case R_X86_64_GOTPCRELX:
-  case R_X86_64_REX_GOTPCRELX:
-  case R_X86_64_CODE_4_GOTPCRELX:
-    relaxed = rel.expr != R_GOT_PC;
-    break;
-  case R_X86_64_GOTPC32_TLSDESC:
-  case R_X86_64_CODE_4_GOTPC32_TLSDESC:
-  case R_X86_64_TLSGD:
-    relaxed = rel.expr == R_TPREL || rel.expr == R_GOT_PC;
-    break;
-  case R_X86_64_TLSLD:
-  case R_X86_64_GOTTPOFF:
-  case R_X86_64_CODE_4_GOTTPOFF:
-  case R_X86_64_CODE_6_GOTTPOFF:
-    relaxed = rel.expr == R_TPREL;
-    break;
-  default:
-    return;
-  }
-  auto &entry = ctx.relaxStats[rel.type];
-  entry.total.fetch_add(1, std::memory_order_relaxed);
-  if (relaxed)
-    entry.relaxed.fetch_add(1, std::memory_order_relaxed);
-}
-
 void X86_64::relocateAlloc(InputSection &sec, uint8_t *buf) const {
   uint64_t secAddr = sec.getOutputSection()->addr + sec.outSecOff;
   for (const Relocation &rel : sec.relocs()) {
@@ -1289,8 +1259,6 @@ void X86_64::relocateAlloc(InputSection &sec, uint8_t *buf) const {
     uint8_t *loc = buf + rel.offset;
     const uint64_t val = sec.getRelocTargetVA(ctx, rel, secAddr + rel.offset);
     relocate(loc, rel, val);
-    if (!ctx.arg.printRelaxStats.empty())
-      trackRelaxStats(ctx, rel);
   }
   if (sec.jumpInstrMod) {
     applyJumpInstrMod(buf + sec.jumpInstrMod->offset,
diff --git a/lld/ELF/Config.h b/lld/ELF/Config.h
index 54a9be3412f8f..69cf9c6d9042c 100644
--- a/lld/ELF/Config.h
+++ b/lld/ELF/Config.h
@@ -736,13 +736,6 @@ struct Ctx : CommonLinkerContext {
   llvm::raw_fd_ostream openAuxiliaryFile(llvm::StringRef, std::error_code &);
 
   std::optional<AArch64PauthAbiCoreInfo> aarch64PauthAbiCoreInfo;
-
-  struct RelaxStatEntry {
-    std::atomic<uint64_t> total{0};
-    std::atomic<uint64_t> relaxed{0};
-  };
-  static constexpr unsigned relaxStatsMaxType = 2048;
-  std::unique_ptr<RelaxStatEntry[]> relaxStats;
 };
 
 // The first two elements of versionDefinitions represent VER_NDX_LOCAL and
diff --git a/lld/ELF/Driver.cpp b/lld/ELF/Driver.cpp
index facdcbcdcc2ae..4861240dae376 100644
--- a/lld/ELF/Driver.cpp
+++ b/lld/ELF/Driver.cpp
@@ -1592,8 +1592,6 @@ static void readConfigs(Ctx &ctx, opt::InputArgList &args) {
   ctx.arg.printMemoryUsage = args.hasArg(OPT_print_memory_usage);
   ctx.arg.printArchiveStats = args.getLastArgValue(OPT_print_archive_stats);
   ctx.arg.printRelaxStats = args.getLastArgValue(OPT_print_relax_stats);
-  if (!ctx.arg.printRelaxStats.empty())
-    ctx.relaxStats.reset(new Ctx::RelaxStatEntry[Ctx::relaxStatsMaxType]());
   ctx.arg.printSymbolOrder = args.getLastArgValue(OPT_print_symbol_order);
   ctx.arg.rejectMismatch = !args.hasArg(OPT_no_warn_mismatch);
   ctx.arg.relax = args.hasFlag(OPT_relax, OPT_no_relax, true);
@@ -2496,7 +2494,7 @@ static void writeArchiveStats(Ctx &ctx) {
 }
 
 static void writeRelaxStats(Ctx &ctx) {
-  if (ctx.arg.printRelaxStats.empty() || !ctx.relaxStats)
+  if (ctx.arg.printRelaxStats.empty())
     return;
 
   std::error_code ec;
@@ -2507,17 +2505,34 @@ static void writeRelaxStats(Ctx &ctx) {
     return;
   }
 
+  // Seed with relaxable relocation types, then single-pass count.
+  llvm::DenseMap<uint32_t, std::pair<uint64_t, uint64_t>> stats;
+  if (ctx.arg.emachine == EM_X86_64)
+    for (RelType type :
+         {R_X86_64_GOTPCRELX, R_X86_64_REX_GOTPCRELX, R_X86_64_CODE_4_GOTPCRELX,
+          R_X86_64_GOTPC32_TLSDESC, R_X86_64_CODE_4_GOTPC32_TLSDESC,
+          R_X86_64_TLSGD, R_X86_64_TLSLD, R_X86_64_GOTTPOFF,
+          R_X86_64_CODE_4_GOTTPOFF, R_X86_64_CODE_6_GOTTPOFF})
+      stats[type] = {0, 0};
+
+  for (InputSectionBase *sec : ctx.inputSections)
+    if (sec && sec->isLive())
+      for (const Relocation &rel : sec->relocs())
+        if (stats.count(rel.type)) {
+          ++stats[rel.type].first;
+          if (rel.relaxed)
+            ++stats[rel.type].second;
+        }
+
   llvm::json::OStream j(os, 2);
   j.object([&] {
     j.attributeObject("relaxations", [&] {
-      for (unsigned i = 0; i < Ctx::relaxStatsMaxType; ++i) {
-        uint64_t total =
-            ctx.relaxStats[i].total.load(std::memory_order_relaxed);
+      for (auto &entry : stats) {
+        uint64_t total = entry.second.first;
         if (total == 0)
           continue;
-        uint64_t relaxed =
-            ctx.relaxStats[i].relaxed.load(std::memory_order_relaxed);
-        j.attributeObject(toStr(ctx, RelType(i)), [&] {
+        uint64_t relaxed = entry.second.second;
+        j.attributeObject(toStr(ctx, RelType(entry.first)), [&] {
           j.attribute("total", int64_t(total));
           j.attribute("relaxed", int64_t(relaxed));
           j.attribute("pct_relaxed", relaxed * 100.0 / total);
diff --git a/lld/ELF/RelocScan.h b/lld/ELF/RelocScan.h
index 4135c15963b95..e21cec6ee1544 100644
--- a/lld/ELF/RelocScan.h
+++ b/lld/ELF/RelocScan.h
@@ -106,7 +106,7 @@ class RelocScan {
                    int64_t addend, Symbol &sym) {
     if (enableIeToLe && !ctx.arg.shared && !sym.isPreemptible) {
       // Optimize to Local Exec.
-      sec->addReloc({R_TPREL, type, offset, addend, &sym});
+      sec->addReloc({R_TPREL, type, offset, addend, &sym, /*relaxed=*/true});
     } else {
       sym.setFlags(NEEDS_TLSIE);
       // R_GOT (absolute GOT address) needs a RELATIVE dynamic relocation in
@@ -130,7 +130,7 @@ class RelocScan {
       return false;
     }
     // Optimize to Local Exec.
-    sec->addReloc({R_TPREL, type, offset, addend, &sym});
+    sec->addReloc({R_TPREL, type, offset, addend, &sym, /*relaxed=*/true});
     return true;
   }
 
@@ -143,10 +143,10 @@ class RelocScan {
       if (sym.isPreemptible) {
         // Optimize to Initial Exec.
         sym.setFlags(NEEDS_TLSIE);
-        sec->addReloc({ieExpr, type, offset, addend, &sym});
+        sec->addReloc({ieExpr, type, offset, addend, &sym, /*relaxed=*/true});
       } else {
         // Optimize to Local Exec.
-        sec->addReloc({leExpr, type, offset, addend, &sym});
+        sec->addReloc({leExpr, type, offset, addend, &sym, /*relaxed=*/true});
       }
       return true;
     }
@@ -166,10 +166,10 @@ class RelocScan {
     } else if (sym.isPreemptible) {
       // Optimize to Initial Exec.
       sym.setFlags(NEEDS_TLSIE);
-      sec->addReloc({ieExpr, type, offset, addend, &sym});
+      sec->addReloc({ieExpr, type, offset, addend, &sym, /*relaxed=*/true});
     } else {
       // Optimize to Local Exec.
-      sec->addReloc({R_TPREL, type, offset, addend, &sym});
+      sec->addReloc({R_TPREL, type, offset, addend, &sym, /*relaxed=*/true});
     }
   }
 };
diff --git a/lld/ELF/Relocations.cpp b/lld/ELF/Relocations.cpp
index 7b4d7217e0452..b1f5d8f9e0094 100644
--- a/lld/ELF/Relocations.cpp
+++ b/lld/ELF/Relocations.cpp
@@ -978,7 +978,8 @@ void RelocScan::processAux(RelExpr expr, RelType type, uint64_t offset,
   // relocation will be created, pass the control to relocateAlloc() or
   // relocateNonAlloc() to resolve it.
   if (isStaticLinkTimeConstant(expr, type, sym, offset)) {
-    sec->addReloc({expr, type, offset, addend, &sym});
+    bool relaxed = oneof<R_RELAX_GOT_PC, R_RELAX_GOT_PC_NOPIC>(expr);
+    sec->addReloc({expr, type, offset, addend, &sym, relaxed});
     return;
   }
 
diff --git a/lld/ELF/Relocations.h b/lld/ELF/Relocations.h
index abc62bf01a421..af52b27e93a1e 100644
--- a/lld/ELF/Relocations.h
+++ b/lld/ELF/Relocations.h
@@ -135,6 +135,7 @@ struct Relocation {
   uint64_t offset;
   int64_t addend;
   Symbol *sym;
+  bool relaxed = false;
 };
 
 // Manipulate jump instructions with these modifiers.  These are used to relax



More information about the llvm-commits mailing list