[lld] [RISCV][LLD] Zcmt RISC-V extension in lld (PR #183450)

via llvm-commits llvm-commits at lists.llvm.org
Mon Apr 13 04:57:02 PDT 2026


https://github.com/LukeZhuang updated https://github.com/llvm/llvm-project/pull/183450

>From 68bbf7fe5116ee20938d144e366ea953a8f1033c Mon Sep 17 00:00:00 2001
From: Zhi Zhuang <zhuangzhi.zz at alibaba-inc.com>
Date: Tue, 7 Oct 2025 20:25:01 +0200
Subject: [PATCH] [lld,RISCV] Add --relax-tbljal to support Zcmt (#183450)

This patch implements the optimization for the Zcmt extension in LLD,
which helps saving notable code-size, especially on the embedded executables.

For more details about Zcmt extension, check the psABI here:
https://riscv.github.io/riscv-isa-manual/snapshot/unprivileged/#insns-tablejump

We scan and collect call/jump relocType in each section before the linker relaxation,
and record the symbols with the bytes they can save each one of them into a table.
Then finalize the table by choosing the most valuable candidates (because the table
size is rather limited). And use this table in the linker relaxation loop to relax
call/jumps whose destination symbol is in our candidate list. Finally we create a
new TableJump section and write that table to it.

This is a continuation of PR #77884 and #163142

Co-authored-by: Craig Topper <craig.topper at sifive.com>
Co-authored-by: VincentWu <43398706+Xinlong-Wu at users.noreply.github.com>
Co-authored-by: Scott Egerton <9487234+ScottEgerton at users.noreply.github.com>
Co-authored-by: Robin Kastberg <Robin.Kastberg at iar.com>
Co-authored-by: Fangrui Song <i at maskray.me>
---
 lld/ELF/Arch/RISCV.cpp                 | 238 ++++++++++++++++++++++++-
 lld/ELF/Config.h                       |   2 +
 lld/ELF/Driver.cpp                     |   1 +
 lld/ELF/Options.td                     |   5 +
 lld/test/ELF/riscv-tbljal-call.s       |  90 ++++++++++
 lld/test/ELF/riscv-tbljal-empty.s      |  35 ++++
 lld/test/ELF/riscv-tbljal-many-jumps.s |  57 ++++++
 lld/test/ELF/riscv-tbljal-syms.s       |  42 +++++
 8 files changed, 464 insertions(+), 6 deletions(-)
 create mode 100644 lld/test/ELF/riscv-tbljal-call.s
 create mode 100644 lld/test/ELF/riscv-tbljal-empty.s
 create mode 100644 lld/test/ELF/riscv-tbljal-many-jumps.s
 create mode 100644 lld/test/ELF/riscv-tbljal-syms.s

diff --git a/lld/ELF/Arch/RISCV.cpp b/lld/ELF/Arch/RISCV.cpp
index 0ded3bb859a37..7508ac0f8a38a 100644
--- a/lld/ELF/Arch/RISCV.cpp
+++ b/lld/ELF/Arch/RISCV.cpp
@@ -9,9 +9,11 @@
 #include "InputFiles.h"
 #include "OutputSections.h"
 #include "RelocScan.h"
+#include "SymbolTable.h"
 #include "Symbols.h"
 #include "SyntheticSections.h"
 #include "Target.h"
+#include "llvm/ADT/MapVector.h"
 #include "llvm/Support/ELFAttributes.h"
 #include "llvm/Support/LEB128.h"
 #include "llvm/Support/RISCVAttributeParser.h"
@@ -32,6 +34,7 @@ class RISCV final : public TargetInfo {
 public:
   RISCV(Ctx &);
   uint32_t calcEFlags() const override;
+  void initTargetSpecificSections() override;
   int64_t getImplicitAddend(const uint8_t *buf, RelType type) const override;
   void writeGotHeader(uint8_t *buf) const override;
   void writeGotPlt(uint8_t *buf, const Symbol &s) const override;
@@ -66,6 +69,34 @@ class RISCV final : public TargetInfo {
   SmallVector<std::pair<uint64_t, uint64_t>, 0> synthesizedAligns;
 };
 
+struct TableJumpEntry {
+  int saved;
+  int index;
+};
+
+// Used by RISC-V Zcmt table jump relaxation.
+class TableJumpSection final : public SyntheticSection {
+public:
+  TableJumpSection(Ctx &);
+  size_t getSize() const override;
+  void writeTo(uint8_t *buf) override;
+
+  void finalizeContents() override;
+  int getCMJTEntryIndex(const Symbol *sym) const;
+  int getCMJALTEntryIndex(const Symbol *sym) const;
+  void addEntry(const Symbol *sym, int saved, bool isCMJT);
+
+private:
+  // Zcmt spec: cm.jt uses indices 0..31, cm.jalt uses indices 32..255.
+  static constexpr size_t maxCMJTEntrySize = 32;
+  static constexpr size_t maxCMJALTEntrySize = 224;
+  static constexpr size_t startCMJALTEntryIdx = 32;
+
+  // Candidate maps: symbol -> (total code size reduction, table index).
+  llvm::MapVector<const Symbol *, TableJumpEntry> cmjtCandidates;
+  llvm::MapVector<const Symbol *, TableJumpEntry> cmjaltCandidates;
+};
+
 } // end anonymous namespace
 
 // These are internal relocation numbers for GP/X0 relaxation. They aren't part
@@ -74,6 +105,7 @@ class RISCV final : public TargetInfo {
 #define INTERNAL_R_RISCV_GPREL_S 257
 #define INTERNAL_R_RISCV_X0REL_I 258
 #define INTERNAL_R_RISCV_X0REL_S 259
+#define INTERNAL_R_RISCV_TBJAL 260
 
 const uint64_t dtpOffset = 0x800;
 
@@ -189,6 +221,18 @@ uint32_t RISCV::calcEFlags() const {
   return target;
 }
 
+void RISCV::initTargetSpecificSections() {
+  if (ctx.arg.relax && ctx.arg.relaxTbljal) {
+    ctx.in.riscvTableJump = std::make_unique<TableJumpSection>(ctx);
+    ctx.inputSections.push_back(ctx.in.riscvTableJump.get());
+
+    Symbol *s = ctx.symtab->addSymbol(Defined{
+        ctx, /*file=*/ctx.internalFile, "__jvt_base$", STB_GLOBAL, STV_DEFAULT,
+        STT_NOTYPE, /*value=*/0, /*size=*/0, ctx.in.riscvTableJump.get()});
+    s->isUsedInRegularObj = true;
+  }
+}
+
 int64_t RISCV::getImplicitAddend(const uint8_t *buf, RelType type) const {
   switch (type) {
   default:
@@ -628,6 +672,9 @@ void RISCV::relocate(uint8_t *loc, const Relocation &rel, uint64_t val) const {
     return;
   }
 
+  case INTERNAL_R_RISCV_TBJAL:
+    return;
+
   case R_RISCV_ADD8:
     *loc += val;
     return;
@@ -890,16 +937,44 @@ void elf::initSymbolAnchors(Ctx &ctx) {
   }
 }
 
+static bool relaxTableJump(Ctx &ctx, const InputSection &sec, size_t i,
+                           uint64_t loc, Relocation &r, uint32_t &remove) {
+  if (!ctx.in.riscvTableJump)
+    return false;
+
+  uint32_t insn = read32le(sec.content().data() + r.offset +
+                           (r.type == R_RISCV_JAL ? 0 : 4));
+  uint8_t rd = extractBits(insn, 11, 7);
+  int tblEntryIndex = -1;
+  auto &tableJump = static_cast<TableJumpSection &>(*ctx.in.riscvTableJump);
+  if (rd == X_X0)
+    tblEntryIndex = tableJump.getCMJTEntryIndex(r.sym);
+  else if (rd == X_RA)
+    tblEntryIndex = tableJump.getCMJALTEntryIndex(r.sym);
+
+  if (tblEntryIndex < 0)
+    return false;
+  sec.relaxAux->relocTypes[i] = INTERNAL_R_RISCV_TBJAL;
+  sec.relaxAux->writes.push_back(0xA002 |
+                                 (tblEntryIndex << 2)); // cm.jt or cm.jalt
+  remove = r.type == R_RISCV_JAL ? 2 : 6;
+  return true;
+}
+
+static int64_t getCallDisplace(Ctx &ctx, const Relocation &r, uint64_t loc) {
+  const Symbol &sym = *r.sym;
+  const uint64_t dest =
+      (r.expr == R_PLT_PC ? sym.getPltVA(ctx) : sym.getVA(ctx)) + r.addend;
+  return dest - loc;
+}
+
 // Relax R_RISCV_CALL/R_RISCV_CALL_PLT auipc+jalr to c.j, c.jal, or jal.
 static void relaxCall(Ctx &ctx, const InputSection &sec, size_t i, uint64_t loc,
                       Relocation &r, uint32_t &remove) {
   const bool rvc = getEFlags(ctx, sec.file) & EF_RISCV_RVC;
-  const Symbol &sym = *r.sym;
   const uint64_t insnPair = read64le(sec.content().data() + r.offset);
   const uint32_t rd = extractBits(insnPair, 32 + 11, 32 + 7);
-  const uint64_t dest =
-      (r.expr == R_PLT_PC ? sym.getPltVA(ctx) : sym.getVA(ctx)) + r.addend;
-  const int64_t displace = dest - loc;
+  const int64_t displace = getCallDisplace(ctx, r, loc);
 
   // When the caller specifies the old value of `remove`, disallow its
   // increment.
@@ -912,6 +987,9 @@ static void relaxCall(Ctx &ctx, const InputSection &sec, size_t i, uint64_t loc,
     sec.relaxAux->relocTypes[i] = R_RISCV_RVC_JUMP;
     sec.relaxAux->writes.push_back(0x2001); // c.jal
     remove = 6;
+  } else if (remove >= (r.type == R_RISCV_JAL ? 2 : 6) &&
+             relaxTableJump(ctx, sec, i, loc, r, remove)) {
+    // relaxTableJump sets remove
   } else if (remove >= 4 && isInt<21>(displace)) {
     sec.relaxAux->relocTypes[i] = R_RISCV_JAL;
     sec.relaxAux->writes.push_back(0x6f | rd << 7); // jal
@@ -1035,6 +1113,10 @@ static bool relax(Ctx &ctx, int pass, InputSection &sec) {
         relaxCall(ctx, sec, i, loc, r, remove);
       }
       break;
+    case R_RISCV_JAL:
+      if (relaxable(relocs, i))
+        relaxTableJump(ctx, sec, i, loc, r, remove);
+      break;
     case R_RISCV_TPREL_HI20:
     case R_RISCV_TPREL_ADD:
     case R_RISCV_TPREL_LO12_I:
@@ -1094,6 +1176,44 @@ static bool relax(Ctx &ctx, int pass, InputSection &sec) {
   return changed;
 }
 
+static void scanTableJumpEntries(Ctx &ctx, const InputSection &sec) {
+  for (auto [i, r] : llvm::enumerate(sec.relocations)) {
+    if (!r.sym->isDefined())
+      continue;
+    if (i + 1 == sec.relocs().size() ||
+        sec.relocs()[i + 1].type != R_RISCV_RELAX)
+      continue;
+    switch (r.type) {
+    case R_RISCV_JAL:
+    case R_RISCV_CALL:
+    case R_RISCV_CALL_PLT: {
+      uint32_t insn = read32le(sec.content().data() + r.offset +
+                               (r.type == R_RISCV_JAL ? 0 : 4));
+      uint8_t rd = extractBits(insn, 11, 7);
+      if (rd != X_X0 && rd != X_RA)
+        break;
+      // Skip candidates that relaxCall will rewrite to c.j or c.jal. Pre-relax
+      // displacement is an upper bound (relaxation only shrinks code), so a
+      // nearby symbol now is still nearby post-relax.
+      const bool rvc = getEFlags(ctx, sec.file) & EF_RISCV_RVC;
+      const int64_t displace = getCallDisplace(ctx, r, sec.getVA() + r.offset);
+      if (rvc && isInt<12>(displace) &&
+          (rd == X_X0 || (rd == X_RA && !ctx.arg.is64)))
+        break;
+      // Use the input instruction size to estimate savings. R_RISCV_CALL/
+      // CALL_PLT are 8-byte auipc+jalr sequences (saving 6 bytes to cm.jt),
+      // while R_RISCV_JAL is already 4 bytes (saving 2 bytes). The actual
+      // saving may be less if the call relaxes to jal first, but this is a
+      // reasonable upper bound for candidate selection.
+      int saved = r.type == R_RISCV_JAL ? 2 : 6;
+      static_cast<TableJumpSection &>(*ctx.in.riscvTableJump)
+          .addEntry(r.sym, saved, /*isCMJT=*/rd == X_X0);
+      break;
+    }
+    }
+  }
+}
+
 // When relaxing just R_RISCV_ALIGN, relocDeltas is usually changed only once in
 // the absence of a linker script. For call and load/store R_RISCV_RELAX, code
 // shrinkage may reduce displacement and make more relocations eligible for
@@ -1103,10 +1223,22 @@ static bool relax(Ctx &ctx, int pass, InputSection &sec) {
 // relaxation pass.
 bool RISCV::relaxOnce(int pass) const {
   llvm::TimeTraceScope timeScope("RISC-V relaxOnce");
-  if (pass == 0)
+  SmallVector<InputSection *, 0> storage;
+  if (pass == 0) {
     initSymbolAnchors(ctx);
 
-  SmallVector<InputSection *, 0> storage;
+    // Scan relocations and build the Zcmt jump table before relaxation.
+    if (ctx.in.riscvTableJump) {
+      for (OutputSection *osec : ctx.outputSections) {
+        if (!(osec->flags & SHF_EXECINSTR))
+          continue;
+        for (InputSection *sec : getInputSections(*osec, storage))
+          scanTableJumpEntries(ctx, *sec);
+      }
+      ctx.in.riscvTableJump->finalizeContents();
+    }
+  }
+
   bool changed = false;
   for (OutputSection *osec : ctx.outputSections) {
     if (!(osec->flags & SHF_EXECINSTR))
@@ -1292,6 +1424,14 @@ void RISCV::finalizeRelax(int passes) const {
           case INTERNAL_R_RISCV_X0REL_I:
           case INTERNAL_R_RISCV_X0REL_S:
             break;
+          case INTERNAL_R_RISCV_TBJAL:
+            assert(ctx.arg.relaxTbljal &&
+                   "TBJAL relocation without --relax-tbljal");
+            assert((aux.writes[writesIdx] & 0xfc03) == 0xA002 &&
+                   "malformed cm.jt/cm.jalt encoding");
+            skip = 2;
+            write16le(p, aux.writes[writesIdx++]);
+            break;
           case R_RISCV_RELAX:
             // Used by relaxTlsLe to indicate the relocation is ignored.
             break;
@@ -1630,3 +1770,89 @@ void elf::mergeRISCVAttributesSections(Ctx &ctx) {
 }
 
 void elf::setRISCVTargetInfo(Ctx &ctx) { ctx.target.reset(new RISCV(ctx)); }
+
+TableJumpSection::TableJumpSection(Ctx &ctx)
+    : SyntheticSection(ctx, ".riscv.jvt", SHT_PROGBITS, SHF_ALLOC,
+                       /*alignment=*/64) {}
+
+int TableJumpSection::getCMJTEntryIndex(const Symbol *sym) const {
+  auto it = cmjtCandidates.find(sym);
+  return it != cmjtCandidates.end() ? it->second.index : -1;
+}
+
+int TableJumpSection::getCMJALTEntryIndex(const Symbol *sym) const {
+  auto it = cmjaltCandidates.find(sym);
+  return it != cmjaltCandidates.end() ? it->second.index : -1;
+}
+
+void TableJumpSection::addEntry(const Symbol *sym, int saved, bool isCMJT) {
+  auto &candidates = isCMJT ? cmjtCandidates : cmjaltCandidates;
+  candidates.try_emplace(sym, TableJumpEntry{0, -1}).first->second.saved +=
+      saved;
+}
+
+// Sort candidates by code size reduction (descending), truncate to maxSize,
+// drop entries whose reduction doesn't cover a table entry, and assign each
+// survivor a table index starting at baseIdx.
+static void
+selectEntries(Ctx &ctx,
+              llvm::MapVector<const Symbol *, TableJumpEntry> &candidates,
+              uint32_t maxSize, uint32_t baseIdx) {
+  SmallVector<std::pair<const Symbol *, TableJumpEntry>, 0> entries(
+      candidates.begin(), candidates.end());
+  // MapVector iteration is in insertion order, so stable_sort gives a
+  // deterministic ordering keyed on saved bytes descending.
+  llvm::stable_sort(entries, [](const auto &a, const auto &b) {
+    return a.second.saved > b.second.saved;
+  });
+  if (entries.size() > maxSize)
+    entries.resize(maxSize);
+  while (!entries.empty() &&
+         entries.back().second.saved < (int)ctx.arg.wordsize)
+    entries.pop_back();
+
+  candidates.clear();
+  for (auto [i, entry] : llvm::enumerate(entries))
+    candidates[entry.first] = {entry.second.saved, (int)(baseIdx + i)};
+}
+
+void TableJumpSection::finalizeContents() {
+  selectEntries(ctx, cmjtCandidates, maxCMJTEntrySize, /*baseIdx=*/0);
+  selectEntries(ctx, cmjaltCandidates, maxCMJALTEntrySize, startCMJALTEntryIdx);
+
+  int cmjtBenefit = 0, cmjaltBenefit = 0;
+  for (auto &[_, e] : cmjtCandidates)
+    cmjtBenefit += e.saved;
+  for (auto &[_, e] : cmjaltCandidates)
+    cmjaltBenefit += e.saved;
+
+  // cm.jalt forces padding the cm.jt region to startCMJALTEntryIdx entries.
+  // Drop cm.jalt if its benefit doesn't cover the padding plus its own entries.
+  size_t pad = startCMJALTEntryIdx - cmjtCandidates.size();
+  if (cmjaltBenefit < (int)((pad + cmjaltCandidates.size()) * ctx.arg.wordsize))
+    cmjaltCandidates.clear();
+
+  // Give up entirely if cm.jt doesn't reduce code size.
+  if (cmjtBenefit <= (int)(cmjtCandidates.size() * ctx.arg.wordsize)) {
+    Log(ctx) << "table jump relaxation didn't reduce code size";
+    cmjtCandidates.clear();
+    cmjaltCandidates.clear();
+  }
+}
+
+size_t TableJumpSection::getSize() const {
+  if (!cmjaltCandidates.empty())
+    return (startCMJALTEntryIdx + cmjaltCandidates.size()) * ctx.arg.wordsize;
+  return cmjtCandidates.size() * ctx.arg.wordsize;
+}
+
+void TableJumpSection::writeTo(uint8_t *buf) {
+  for (auto *cands : {&cmjtCandidates, &cmjaltCandidates})
+    for (auto &[sym, entry] : *cands) {
+      uint8_t *p = buf + entry.index * ctx.arg.wordsize;
+      if (ctx.arg.is64)
+        write64le(p, sym->getVA(ctx));
+      else
+        write32le(p, sym->getVA(ctx));
+    }
+}
diff --git a/lld/ELF/Config.h b/lld/ELF/Config.h
index cf3cde1cf2696..da58382ee08bb 100644
--- a/lld/ELF/Config.h
+++ b/lld/ELF/Config.h
@@ -371,6 +371,7 @@ struct Config {
   bool resolveGroups;
   bool relrGlibc = false;
   bool relrPackDynRelocs = false;
+  bool relaxTbljal;
   llvm::DenseSet<llvm::StringRef> saveTempsArgs;
   llvm::SmallVector<std::pair<llvm::GlobPattern, uint32_t>, 0> shuffleSections;
   bool singleRoRx;
@@ -579,6 +580,7 @@ struct InStruct {
   std::unique_ptr<RelroPaddingSection> relroPadding;
   std::unique_ptr<SyntheticSection> armCmseSGSection;
   std::unique_ptr<PPC64LongBranchTargetSection> ppc64LongBranchTarget;
+  std::unique_ptr<SyntheticSection> riscvTableJump;
   std::unique_ptr<SyntheticSection> mipsAbiFlags;
   std::unique_ptr<MipsGotSection> mipsGot;
   std::unique_ptr<SyntheticSection> mipsOptions;
diff --git a/lld/ELF/Driver.cpp b/lld/ELF/Driver.cpp
index 24718d2d2186a..19fc5d764184e 100644
--- a/lld/ELF/Driver.cpp
+++ b/lld/ELF/Driver.cpp
@@ -1594,6 +1594,7 @@ static void readConfigs(Ctx &ctx, opt::InputArgList &args) {
   ctx.arg.rejectMismatch = !args.hasArg(OPT_no_warn_mismatch);
   ctx.arg.relax = args.hasFlag(OPT_relax, OPT_no_relax, true);
   ctx.arg.relaxGP = args.hasFlag(OPT_relax_gp, OPT_no_relax_gp, false);
+  ctx.arg.relaxTbljal = args.hasArg(OPT_relax_tbljal);
   ctx.arg.rpath = getRpath(args);
   ctx.arg.relocatable = args.hasArg(OPT_relocatable);
   ctx.arg.resolveGroups =
diff --git a/lld/ELF/Options.td b/lld/ELF/Options.td
index 64c42eb49607d..42f7bb3532001 100644
--- a/lld/ELF/Options.td
+++ b/lld/ELF/Options.td
@@ -426,6 +426,11 @@ defm relax_gp: BB<"relax-gp",
   "Enable global pointer relaxation",
   "Disable global pointer relaxation (default)">;
 
+def relax_tbljal : FF<"relax-tbljal">,
+                   HelpText<"Enable conversion of call instructions to table "
+                            "jump instruction from the Zcmt extension for "
+                            "frequently called functions (RISC-V only)">;
+
 defm remap_inputs: EEq<"remap-inputs",
   "Remap input files matching <from-glob> to <to-file>">,
   MetaVarName<"<from-glob>=<to-file>">;
diff --git a/lld/test/ELF/riscv-tbljal-call.s b/lld/test/ELF/riscv-tbljal-call.s
new file mode 100644
index 0000000000000..dd2d1e8abcf52
--- /dev/null
+++ b/lld/test/ELF/riscv-tbljal-call.s
@@ -0,0 +1,90 @@
+# REQUIRES: riscv
+
+## Test that call/tail instructions are relaxed to cm.jt/cm.jalt when
+## --relax-tbljal is enabled and the table jump is profitable.
+
+# RUN: llvm-mc -filetype=obj -triple=riscv32 -mattr=+relax,+zcmt %s -o %t.rv32.o
+# RUN: llvm-mc -filetype=obj -triple=riscv64 -mattr=+relax,+zcmt %s -o %t.rv64.o
+# RUN: ld.lld %t.rv32.o --relax-tbljal --defsym foo=0x150000 --defsym foo_1=0x150010 --defsym foo_3=0x150030 -o %t.rv32
+# RUN: ld.lld %t.rv64.o --relax-tbljal --defsym foo=0x150000 --defsym foo_1=0x150010 --defsym foo_3=0x150030 -o %t.rv64
+
+## Check disassembly for cm.jalt (rd=ra) and cm.jt (rd=zero).
+# RUN: llvm-objdump -d -M no-aliases --mattr=+zcmt --no-show-raw-insn %t.rv32 | FileCheck --check-prefix=RV32 %s
+# RUN: llvm-objdump -d -M no-aliases --mattr=+zcmt --no-show-raw-insn %t.rv64 | FileCheck --check-prefix=RV64 %s
+
+## Check jump table contents.
+# RUN: llvm-readelf -x .riscv.jvt %t.rv32 | FileCheck --check-prefix=JVT32 %s
+# RUN: llvm-readelf -x .riscv.jvt %t.rv64 | FileCheck --check-prefix=JVT64 %s
+
+## --no-relax disables all linker relaxation, including table jump.
+## Verify the .riscv.jvt section is not created and no cm.jt/cm.jalt emitted.
+# RUN: ld.lld %t.rv32.o --no-relax --relax-tbljal --defsym foo=0x150000 --defsym foo_1=0x150010 --defsym foo_3=0x150030 -o %t.norelax
+# RUN: llvm-readelf -S %t.norelax | FileCheck --check-prefix=NORELAX-SEC %s
+# RUN: llvm-objdump -d --mattr=+zcmt --no-show-raw-insn %t.norelax | FileCheck --check-prefix=NORELAX %s
+# NORELAX-SEC-NOT: .riscv.jvt
+# NORELAX-NOT:     cm.jt
+# NORELAX-NOT:     cm.jalt
+
+## RV32 -- 21 calls to foo become cm.jalt at index 0x20 (first cm.jalt slot).
+## Tails become cm.jt with indices assigned by saved-bytes descending, tiebreaking
+## on symbol name: foo_3 (saved=30) -> 0x0, foo_1 (saved=18) -> 0x1,
+## foo and foo_2 both saved=6 -> 0x2 and 0x3 (alphabetical).
+# RV32-NOT:         cm.jt
+# RV32-NOT:         cm.jalt
+# RV32-COUNT-21:    cm.jalt 0x20
+# RV32-NEXT:        cm.jt   0x2
+# RV32-NEXT:        cm.jt   0x1
+# RV32-NEXT:        cm.jt   0x1
+# RV32-NEXT:        cm.jt   0x1
+# RV32-NEXT:        cm.jt   0x0
+# RV32-NEXT:        c.j
+# RV32-NEXT:        cm.jt   0x0
+# RV32-NEXT:        cm.jt   0x0
+# RV32-NEXT:        cm.jt   0x0
+# RV32-NEXT:        cm.jt   0x0
+# RV32-NOT:         cm.jt
+# RV32-NOT:         cm.jalt
+
+## RV64 -- no cm.jalt (CMJALT padding cost with 8-byte entries is too high).
+## Only tails are relaxed to cm.jt: foo_3 -> 0x0, foo_1 -> 0x1.
+# RV64-NOT:         cm.jalt
+# RV64:             cm.jt   0x1
+# RV64-NEXT:        cm.jt   0x1
+# RV64-NEXT:        cm.jt   0x1
+# RV64-NEXT:        cm.jt   0x0
+# RV64-NEXT:        c.j
+# RV64-NEXT:        cm.jt   0x0
+# RV64-NEXT:        cm.jt   0x0
+# RV64-NEXT:        cm.jt   0x0
+# RV64-NEXT:        cm.jt   0x0
+# RV64-NOT:         cm.jt
+# RV64-NOT:         cm.jalt
+
+## Verify table entries contain the target addresses (little-endian).
+## RV32 CMJT -- foo_3 (idx 0), foo_1 (idx 1), foo (idx 2), foo_2 (idx 3);
+## then padding to idx 32; then CMJALT for foo.
+# JVT32: 30001500 10001500 00001500
+# JVT32: 00001500
+
+## RV64 CMJT -- foo_3 (idx 0), foo_1 (idx 1). No CMJALT.
+# JVT64: 30001500 00000000 10001500 00000000
+
+.global _start
+.p2align 3
+_start:
+  .rept 21
+  call foo
+  .endr
+  tail foo
+  tail foo_1
+  tail foo_1
+  tail foo_1
+  tail foo_3
+  tail foo_2
+  tail foo_3
+  tail foo_3
+  tail foo_3
+  tail foo_3
+
+foo_2:
+  nop
diff --git a/lld/test/ELF/riscv-tbljal-empty.s b/lld/test/ELF/riscv-tbljal-empty.s
new file mode 100644
index 0000000000000..7cafbf1dd15b0
--- /dev/null
+++ b/lld/test/ELF/riscv-tbljal-empty.s
@@ -0,0 +1,35 @@
+# REQUIRES: riscv
+
+## When there are too few calls, table jump relaxation should not be profitable.
+## Verify the .riscv.jvt section has zero size and no cm.jt/cm.jalt are emitted.
+
+# RUN: llvm-mc -filetype=obj -triple=riscv32 -mattr=+relax,+zcmt %s -o %t.rv32.o
+# RUN: llvm-mc -filetype=obj -triple=riscv64 -mattr=+relax,+zcmt %s -o %t.rv64.o
+# RUN: ld.lld %t.rv32.o --relax-tbljal --defsym foo=0x150000 --verbose -o %t.rv32 2>&1 | FileCheck --check-prefix=LOG %s
+# RUN: ld.lld %t.rv64.o --relax-tbljal --defsym foo=0x150000 -o %t.rv64
+# RUN: llvm-readelf -S %t.rv32 | FileCheck --check-prefix=SEC32 %s
+# RUN: llvm-readelf -S %t.rv64 | FileCheck --check-prefix=SEC64 %s
+# RUN: llvm-objdump -d --mattr=+zcmt --no-show-raw-insn %t.rv32 | FileCheck --check-prefix=DISASM %s
+# RUN: llvm-objdump -d --mattr=+zcmt --no-show-raw-insn %t.rv64 | FileCheck --check-prefix=DISASM %s
+
+# LOG: table jump relaxation didn't reduce code size
+
+# SEC32: .riscv.jvt PROGBITS {{[0-9a-f]+}} {{[0-9a-f]+}} 000000
+# SEC64: .riscv.jvt PROGBITS {{[0-9a-f]+}} {{[0-9a-f]+}} 000000
+
+# DISASM-NOT: cm.jt
+# DISASM-NOT: cm.jalt
+
+.global _start
+_start:
+  call foo
+  tail foo_1
+  tail foo_2
+  tail foo_3
+
+foo_1:
+  nop
+foo_2:
+  nop
+foo_3:
+  nop
diff --git a/lld/test/ELF/riscv-tbljal-many-jumps.s b/lld/test/ELF/riscv-tbljal-many-jumps.s
new file mode 100644
index 0000000000000..33f9042326eb0
--- /dev/null
+++ b/lld/test/ELF/riscv-tbljal-many-jumps.s
@@ -0,0 +1,57 @@
+# REQUIRES: riscv
+
+## Test table jump with many targets filling the cm.jt table (32 entries).
+## Verify the .riscv.jvt section size accounts for all entries.
+
+# RUN: llvm-mc -filetype=obj -triple=riscv32 -mattr=+relax,+zcmt -asm-macro-max-nesting-depth=33 %s -o %t.rv32.o
+# RUN: llvm-mc -filetype=obj -triple=riscv64 -mattr=+relax,+zcmt -asm-macro-max-nesting-depth=33 %s -o %t.rv64.o
+
+## Use --defsym for targets beyond c.j range so they can only be relaxed via table jump.
+# RUN: ld.lld %t.rv32.o --relax-tbljal \
+# RUN:   --defsym t0=0x100000 --defsym t1=0x100010 --defsym t2=0x100020 --defsym t3=0x100030 \
+# RUN:   --defsym t4=0x100040 --defsym t5=0x100050 --defsym t6=0x100060 --defsym t7=0x100070 \
+# RUN:   --defsym t8=0x100080 --defsym t9=0x100090 --defsym t10=0x1000a0 --defsym t11=0x1000b0 \
+# RUN:   --defsym t12=0x1000c0 --defsym t13=0x1000d0 --defsym t14=0x1000e0 --defsym t15=0x1000f0 \
+# RUN:   --defsym t16=0x100100 --defsym t17=0x100110 --defsym t18=0x100120 --defsym t19=0x100130 \
+# RUN:   --defsym t20=0x100140 --defsym t21=0x100150 --defsym t22=0x100160 --defsym t23=0x100170 \
+# RUN:   --defsym t24=0x100180 --defsym t25=0x100190 --defsym t26=0x1001a0 --defsym t27=0x1001b0 \
+# RUN:   --defsym t28=0x1001c0 --defsym t29=0x1001d0 --defsym t30=0x1001e0 --defsym t31=0x1001f0 \
+# RUN:   -o %t.rv32
+# RUN: ld.lld %t.rv64.o --relax-tbljal \
+# RUN:   --defsym t0=0x100000 --defsym t1=0x100010 --defsym t2=0x100020 --defsym t3=0x100030 \
+# RUN:   --defsym t4=0x100040 --defsym t5=0x100050 --defsym t6=0x100060 --defsym t7=0x100070 \
+# RUN:   --defsym t8=0x100080 --defsym t9=0x100090 --defsym t10=0x1000a0 --defsym t11=0x1000b0 \
+# RUN:   --defsym t12=0x1000c0 --defsym t13=0x1000d0 --defsym t14=0x1000e0 --defsym t15=0x1000f0 \
+# RUN:   --defsym t16=0x100100 --defsym t17=0x100110 --defsym t18=0x100120 --defsym t19=0x100130 \
+# RUN:   --defsym t20=0x100140 --defsym t21=0x100150 --defsym t22=0x100160 --defsym t23=0x100170 \
+# RUN:   --defsym t24=0x100180 --defsym t25=0x100190 --defsym t26=0x1001a0 --defsym t27=0x1001b0 \
+# RUN:   --defsym t28=0x1001c0 --defsym t29=0x1001d0 --defsym t30=0x1001e0 --defsym t31=0x1001f0 \
+# RUN:   -o %t.rv64
+
+# RUN: llvm-readelf -S %t.rv32 | FileCheck --check-prefix=SEC32 %s
+# RUN: llvm-readelf -S %t.rv64 | FileCheck --check-prefix=SEC64 %s
+# RUN: llvm-objdump -d --mattr=+zcmt --no-show-raw-insn %t.rv32 | FileCheck --check-prefix=DISASM %s
+
+## 32 entries * 4 bytes = 0x80; 32 entries * 8 bytes = 0x100.
+# SEC32: .riscv.jvt PROGBITS {{[0-9a-f]+}} {{[0-9a-f]+}} 000080
+# SEC64: .riscv.jvt PROGBITS {{[0-9a-f]+}} {{[0-9a-f]+}} 000100
+
+## Verify some instructions were converted.
+# DISASM: cm.jt
+
+.global _start
+.p2align 3
+_start:
+## Use enough repetitions per target so that the savings (2 bytes per tail on
+## RV64 after jal relaxation) exceed the table entry cost (8 bytes on RV64).
+.altmacro
+.macro iota n, i=0
+.if \n-\i
+  .rept 6
+  tail t\i
+  .endr
+  iota \n, %(\i+1)
+.endif
+.endm
+
+iota 32
diff --git a/lld/test/ELF/riscv-tbljal-syms.s b/lld/test/ELF/riscv-tbljal-syms.s
new file mode 100644
index 0000000000000..dea036c59b421
--- /dev/null
+++ b/lld/test/ELF/riscv-tbljal-syms.s
@@ -0,0 +1,42 @@
+# REQUIRES: riscv
+
+## Check that relaxation correctly adjusts symbol addresses and sizes.
+
+# RUN: llvm-mc -filetype=obj -triple=riscv32 -mattr=+relax,+zcmt %s -o %t.rv32.o
+# RUN: llvm-mc -filetype=obj -triple=riscv64 -mattr=+relax,+zcmt %s -o %t.rv64.o
+# RUN: ld.lld -Ttext=0x100000 --relax-tbljal %t.rv32.o -o %t.rv32
+# RUN: ld.lld -Ttext=0x100000 --relax-tbljal %t.rv64.o -o %t.rv64
+
+# RUN: llvm-readelf -s %t.rv32 | FileCheck --check-prefix=CHECK32 %s
+# RUN: llvm-readelf -s %t.rv64 | FileCheck --check-prefix=CHECK64 %s
+
+# CHECK32:      00100000     4 NOTYPE  LOCAL  DEFAULT     1 a
+# CHECK32-NEXT: 00100000     6 NOTYPE  LOCAL  DEFAULT     1 b
+# CHECK32-NEXT: 00100000     0 NOTYPE  LOCAL  DEFAULT     1 $x
+# CHECK32-NEXT: 00100004     2 NOTYPE  LOCAL  DEFAULT     1 c
+# CHECK32-NEXT: 00100004     6 NOTYPE  LOCAL  DEFAULT     1 d
+# CHECK32-NEXT: 00100000    10 NOTYPE  GLOBAL DEFAULT     1 _start
+# CHECK32:                     NOTYPE  GLOBAL DEFAULT   {{.*}} __jvt_base$
+
+# CHECK64:      00100000     4 NOTYPE  LOCAL  DEFAULT     1 a
+# CHECK64-NEXT: 00100000     8 NOTYPE  LOCAL  DEFAULT     1 b
+# CHECK64-NEXT: 00100000     0 NOTYPE  LOCAL  DEFAULT     1 $x
+# CHECK64-NEXT: 00100004     4 NOTYPE  LOCAL  DEFAULT     1 c
+# CHECK64-NEXT: 00100004     8 NOTYPE  LOCAL  DEFAULT     1 d
+# CHECK64-NEXT: 00100000    12 NOTYPE  GLOBAL DEFAULT     1 _start
+# CHECK64:                     NOTYPE  GLOBAL DEFAULT   {{.*}} __jvt_base$
+
+.global _start
+_start:
+a:
+b:
+  add  a0, a1, a2
+.size a, . - a
+c:
+d:
+  call _start
+.size b, . - b
+.size c, . - c
+  add a0, a1, a2
+.size d, . - d
+.size _start, . - _start



More information about the llvm-commits mailing list