[llvm] [SampleProfile] Support Eytzinger layout in SecFuncOffsetTable (PR #213829)

Kazu Hirata via llvm-commits llvm-commits at lists.llvm.org
Mon Aug 10 00:03:55 PDT 2026


https://github.com/kazutakahirata updated https://github.com/llvm/llvm-project/pull/213829

>From df0b27fa831b4c31ef184815b6b9298ca27e923c Mon Sep 17 00:00:00 2001
From: Kazu Hirata <kazu at google.com>
Date: Fri, 31 Jul 2026 20:46:51 -0700
Subject: [PATCH 1/3] [SampleProfile] Support Eytzinger layout in
 SecFuncOffsetTable

This patch supports writing SecFuncOffsetTable as an array of
fixed-length uint32_t file offsets parallel to the Eytzinger spans in
SecNameTable for ExtBinary sample profiles.

Without this patch, SecFuncOffsetTable stores pairs of variable-length
ULEB128 integers for GUIDs and file offsets, requiring us to eagerly
load and decode the entire section into an in-memory map.

This patch implements parallel arrays to avoid eager loading:

- In the writer, when -sample-profile-write-eytzinger-name-tables is set,
  SecFuncOffsetTable holds only an array of uint32_t offsets into
  SecLBRProfile. The k-th offset entry corresponds to the k-th GUID in the
  CSKeys or FlatKeys Eytzinger span of SecNameTable.

- In the reader, when SecFlagEytzinger is set on SecFuncOffsetTable, the
  reader accesses the memory-mapped offset array directly without eagerly
  decoding ULEB128 pairs or constructing an in-memory map.

This patch does not add a unit test but renames an existing one because
the test that is being renamed already covers the roundtrip test with
-sample-profile-write-eytzinger-name-tables set.

RFC:
https://discourse.llvm.org/t/rfc-faster-sample-profile-loading/90957/8

Assisted-by: Antigravity
---
 llvm/include/llvm/ProfileData/SampleProf.h    |  3 +
 .../llvm/ProfileData/SampleProfReader.h       | 52 ++++++++++++-
 .../llvm/ProfileData/SampleProfWriter.h       |  6 +-
 llvm/lib/ProfileData/SampleProfReader.cpp     | 62 ++++++++++++++-
 llvm/lib/ProfileData/SampleProfWriter.cpp     | 75 +++++++++++++++++--
 .../eytzinger-split-nametable-partition.test  |  4 +-
 6 files changed, 189 insertions(+), 13 deletions(-)

diff --git a/llvm/include/llvm/ProfileData/SampleProf.h b/llvm/include/llvm/ProfileData/SampleProf.h
index c409b4293ee8d..eaf347b62b24e 100644
--- a/llvm/include/llvm/ProfileData/SampleProf.h
+++ b/llvm/include/llvm/ProfileData/SampleProf.h
@@ -250,6 +250,9 @@ enum class SecFuncOffsetFlags : uint32_t {
   // Store function offsets in an order of contexts. The order ensures that
   // callee contexts of a given context laid out next to it.
   SecFlagOrdered = (1 << 0),
+  // Store function offsets in a parallel array aligned with Eytzinger NameTable
+  // span.
+  SecFlagEytzinger = (1 << 1),
 };
 
 // Verify section specific flag is used for the correct section.
diff --git a/llvm/include/llvm/ProfileData/SampleProfReader.h b/llvm/include/llvm/ProfileData/SampleProfReader.h
index 2ce052d5fb0a5..6eb5760d8e1e6 100644
--- a/llvm/include/llvm/ProfileData/SampleProfReader.h
+++ b/llvm/include/llvm/ProfileData/SampleProfReader.h
@@ -397,6 +397,12 @@ class SampleProfileNameTable {
   virtual size_t size() const = 0;
   bool empty() const { return size() == 0; }
   virtual FunctionId operator[](size_t Idx) const = 0;
+
+  virtual EytzingerTableSpan<support::ulittle64_t>
+  getEytzingerSpan(bool IsCS) const {
+    llvm_unreachable(
+        "getEytzingerSpan is exclusively supported for Eytzinger layout");
+  }
   virtual bool contains(StringRef Key) const {
     return contains(FunctionId(Key).getHashCode());
   }
@@ -514,6 +520,12 @@ class EytzingerSampleProfileNameTable final : public SampleProfileNameTable {
     return FunctionId(Array[Idx]);
   }
 
+  EytzingerTableSpan<support::ulittle64_t>
+  getEytzingerSpan(bool IsCS) const override {
+    return Spans[static_cast<size_t>(IsCS ? EytzingerSpan::CS
+                                          : EytzingerSpan::Flat)];
+  }
+
   bool contains(uint64_t GUID) const override {
     return llvm::any_of(Spans,
                         [&](const auto &Span) { return Span.contains(GUID); });
@@ -1004,8 +1016,11 @@ class FuncOffsetHashTableInfo {
 /// Tags to select the initialization mode of SampleProfileFuncOffsetTable.
 struct InMemoryModeT {};
 struct OnDiskModeT {};
+struct EytzingerModeT {};
+
 inline constexpr InMemoryModeT InMemoryMode{};
 inline constexpr OnDiskModeT OnDiskMode{};
+inline constexpr EytzingerModeT EytzingerMode{};
 
 /// A unified wrapper representing the function offset table.
 ///
@@ -1019,6 +1034,8 @@ inline constexpr OnDiskModeT OnDiskMode{};
 /// - An OnDiskIterableChainedHashTable providing the same mapping directly from
 ///   the file in (non-context-sensitive) version 104 profiles.
 ///
+/// - A raw slice of 32-bit relative offsets for Eytzinger parallel lookups.
+///
 /// It exposes a single, type-agnostic lookup interface, shielding the reader
 /// from the underlying container types. To prevent hybrid-state corruption, the
 /// table's mode is locked at construction time, and assertions prevent
@@ -1041,6 +1058,11 @@ class SampleProfileFuncOffsetTable {
     InMemoryTable.reserve(InitialCapacity);
   }
 
+  SampleProfileFuncOffsetTable(
+      EytzingerModeT, EytzingerTableSpan<support::ulittle64_t> NameSpan,
+      ArrayRef<support::ulittle32_t> FuncOffsetSpan)
+      : NameSpan(NameSpan), FuncOffsetSpan(FuncOffsetSpan) {}
+
   /// Insert a function GUID and its profile offset into the in-memory map.
   /// Enforces that the on-disk table must not have been set first.
   void insert(uint64_t GUID, uint64_t Offset) {
@@ -1058,6 +1080,14 @@ class SampleProfileFuncOffsetTable {
   /// Query the offset table for the profile offset associated with the given
   /// GUID. Returns the offset if found, or std::nullopt if the key is missing.
   std::optional<uint64_t> lookup(uint64_t GUID) const {
+    if (isEytzinger()) {
+      if (std::optional<size_t> Idx = NameSpan.findIndex(GUID)) {
+        uint32_t RelOffset = FuncOffsetSpan[*Idx];
+        if (RelOffset != UINT32_MAX)
+          return RelOffset;
+      }
+      return std::nullopt;
+    }
     if (OnDiskTable) {
       auto Iter = OnDiskTable->find(GUID);
       if (Iter != OnDiskTable->end())
@@ -1070,9 +1100,27 @@ class SampleProfileFuncOffsetTable {
     return std::nullopt;
   }
 
+  /// Direct read-only array (`ArrayRef`) of function offsets aligned parallel
+  /// to the corresponding Eytzinger name span:
+  ArrayRef<support::ulittle32_t> getFuncOffsets() const {
+    assert(isEytzinger() &&
+           "Cannot call getFuncOffsets() on non-Eytzinger table");
+    return FuncOffsetSpan;
+  }
+
+  size_t getExpectedSize() const {
+    assert(isEytzinger() &&
+           "Cannot call getExpectedSize() on non-Eytzinger table");
+    return NameSpan.size();
+  }
+
+  bool isEytzinger() const { return FuncOffsetSpan.data() != nullptr; }
+
 private:
   llvm::DenseMap<hash_code, uint64_t> InMemoryTable;
   std::unique_ptr<OnDiskTableType> OnDiskTable;
+  EytzingerTableSpan<support::ulittle64_t> NameSpan;
+  ArrayRef<support::ulittle32_t> FuncOffsetSpan;
 };
 
 /// SampleProfileReaderExtBinaryBase/SampleProfileWriterExtBinaryBase defines
@@ -1112,7 +1160,9 @@ class LLVM_ABI SampleProfileReaderExtBinaryBase
   std::error_code readFuncMetadata(DenseSet<FunctionSamples *> &Profiles);
   std::error_code readFuncMetadata();
   std::error_code readFuncMetadata(FunctionSamples *FProfile);
-  std::error_code readFuncOffsetTable();
+  std::error_code readFuncOffsetTable(bool IsEytzinger, bool IsCS);
+  std::error_code readEytzingerFuncOffsetTable(bool IsCS);
+  std::error_code readLegacyFuncOffsetTable();
   std::error_code readFuncProfiles();
   std::error_code readFuncProfiles(const DenseSet<StringRef> &FuncsToUse,
                                    SampleProfileMap &Profiles);
diff --git a/llvm/include/llvm/ProfileData/SampleProfWriter.h b/llvm/include/llvm/ProfileData/SampleProfWriter.h
index ef368094d5889..b9797c458617a 100644
--- a/llvm/include/llvm/ProfileData/SampleProfWriter.h
+++ b/llvm/include/llvm/ProfileData/SampleProfWriter.h
@@ -417,7 +417,9 @@ class LLVM_ABI SampleProfileWriterExtBinaryBase
   std::error_code writeNameTableSection(const SampleProfileMap &ProfileMap);
   std::error_code
   writeEytzingerNameTableSection(const SampleProfileMap &ProfileMap);
-  std::error_code writeFuncOffsetTable();
+  std::error_code writeFuncOffsetTable(bool IsCS);
+  std::error_code writeEytzingerFuncOffsetTable(bool IsCS);
+  std::error_code writeLegacyFuncOffsetTable();
   std::error_code writeProfileSymbolListSection();
   std::error_code writeStringBasedProfileSymbolListSection();
   std::error_code writeMD5ProfileSymbolListSection();
@@ -466,6 +468,8 @@ class LLVM_ABI SampleProfileWriterExtBinaryBase
   MapVector<SampleContext, uint64_t> FuncOffsetTable;
   // Whether to use MD5 to represent string.
   bool UseMD5 = false;
+  size_t NumCS = 0;
+  size_t NumFlat = 0;
 
   /// CSNameTable maps function context to its offset in SecCSNameTable section.
   /// The offset will be used everywhere where the context is referenced.
diff --git a/llvm/lib/ProfileData/SampleProfReader.cpp b/llvm/lib/ProfileData/SampleProfReader.cpp
index f7e291729a5d9..eb8e064da198e 100644
--- a/llvm/lib/ProfileData/SampleProfReader.cpp
+++ b/llvm/lib/ProfileData/SampleProfReader.cpp
@@ -895,10 +895,18 @@ std::error_code SampleProfileReaderExtBinaryBase::readOneSection(
     if (!M) {
       Data = End;
     } else {
+      bool IsEytzinger =
+          hasSecFlag(Entry, SecFuncOffsetFlags::SecFlagEytzinger);
+      bool IsFlat = hasSecFlag(Entry, SecCommonFlags::SecFlagFlat);
+      // An unflagged function offset table inherently indexes the primary
+      // context-sensitive symbol span.
+      bool IsCS = !IsFlat;
       assert((!ProfileIsCS ||
-              hasSecFlag(Entry, SecFuncOffsetFlags::SecFlagOrdered)) &&
-             "func offset table should always be sorted in CS profile");
-      if (std::error_code EC = readFuncOffsetTable())
+              hasSecFlag(Entry, SecFuncOffsetFlags::SecFlagOrdered) ||
+              IsEytzinger) &&
+             "func offset table should always be sorted or in Eytzinger BFS "
+             "order in CS profile");
+      if (std::error_code EC = readFuncOffsetTable(IsEytzinger, IsCS))
         return EC;
     }
     break;
@@ -985,7 +993,36 @@ bool SampleProfileReaderExtBinaryBase::collectFuncsFromModule() {
   return true;
 }
 
-std::error_code SampleProfileReaderExtBinaryBase::readFuncOffsetTable() {
+std::error_code
+SampleProfileReaderExtBinaryBase::readFuncOffsetTable(bool IsEytzinger,
+                                                      bool IsCS) {
+  if (IsEytzinger)
+    return readEytzingerFuncOffsetTable(IsCS);
+  return readLegacyFuncOffsetTable();
+}
+
+std::error_code
+SampleProfileReaderExtBinaryBase::readEytzingerFuncOffsetTable(bool IsCS) {
+  // If there are more than one function offset section, the profile associated
+  // with the previous section has to be done reading before next one is read.
+  FuncOffsetTable.reset();
+
+  size_t Size = End - Data;
+  size_t SpanSize = NameTable->getEytzingerSpan(IsCS).size();
+  if (Size != SpanSize * sizeof(uint32_t))
+    return sampleprof_error::malformed;
+
+  auto *Array = reinterpret_cast<const support::ulittle32_t *>(Data);
+  ArrayRef<support::ulittle32_t> Offsets(Array, SpanSize);
+
+  FuncOffsetTable.emplace(EytzingerMode, NameTable->getEytzingerSpan(IsCS),
+                          Offsets);
+
+  Data = End;
+  return sampleprof_error::success;
+}
+
+std::error_code SampleProfileReaderExtBinaryBase::readLegacyFuncOffsetTable() {
   // If there are more than one function offset section, the profile associated
   // with the previous section has to be done reading before next one is read.
   FuncOffsetTable.reset();
@@ -1032,6 +1069,21 @@ std::error_code SampleProfileReaderExtBinaryBase::readFuncProfiles(
     }
   }
 
+  if (FuncOffsetTable && FuncOffsetTable->isEytzinger() &&
+      useFuncOffsetList()) {
+    ArrayRef<support::ulittle32_t> Offsets = FuncOffsetTable->getFuncOffsets();
+    if (Offsets.size() != FuncOffsetTable->getExpectedSize())
+      return sampleprof_error::malformed;
+    for (const auto &[LocalIdx, RelOffset] : llvm::enumerate(Offsets)) {
+      if (RelOffset == UINT32_MAX)
+        continue;
+      const uint8_t *FuncProfileAddr = Start + RelOffset;
+      if (std::error_code EC = readFuncProfile(FuncProfileAddr, Profiles))
+        return EC;
+    }
+    return sampleprof_error::success;
+  }
+
   if (ProfileIsCS) {
     assert(useFuncOffsetList());
     DenseSet<uint64_t> FuncGuidsToUse;
@@ -1669,6 +1721,8 @@ static std::string getSecFlagsStr(const SecHdrTableEntry &Entry) {
   case SecFuncOffsetTable:
     if (hasSecFlag(Entry, SecFuncOffsetFlags::SecFlagOrdered))
       Flags.append("ordered,");
+    if (hasSecFlag(Entry, SecFuncOffsetFlags::SecFlagEytzinger))
+      Flags.append("eytzinger,");
     break;
   case SecFuncMetadata:
     if (hasSecFlag(Entry, SecFuncMetadataFlags::SecFlagIsProbeBased))
diff --git a/llvm/lib/ProfileData/SampleProfWriter.cpp b/llvm/lib/ProfileData/SampleProfWriter.cpp
index e17b080659927..4d87f376702ae 100644
--- a/llvm/lib/ProfileData/SampleProfWriter.cpp
+++ b/llvm/lib/ProfileData/SampleProfWriter.cpp
@@ -58,7 +58,8 @@ static cl::opt<bool>
 
 static cl::opt<bool> WriteEytzingerNameTables(
     "sample-profile-write-eytzinger-name-tables", cl::init(false), cl::Hidden,
-    cl::desc("Write Eytzinger 3-span layout for NameTable"));
+    cl::desc("Write Eytzinger 3-span layout for NameTable and parallel "
+             "FuncOffsetTable"));
 
 namespace llvm {
 namespace support {
@@ -277,7 +278,57 @@ SampleProfileWriterExtBinaryBase::writeSample(const FunctionSamples &S) {
   return writeBody(S);
 }
 
-std::error_code SampleProfileWriterExtBinaryBase::writeFuncOffsetTable() {
+std::error_code
+SampleProfileWriterExtBinaryBase::writeFuncOffsetTable(bool IsCS) {
+  if (WriteEytzingerNameTables)
+    return writeEytzingerFuncOffsetTable(IsCS);
+  return writeLegacyFuncOffsetTable();
+}
+
+std::error_code
+SampleProfileWriterExtBinaryBase::writeEytzingerFuncOffsetTable(bool IsCS) {
+  assert((NumCS + NumFlat > 0 || FuncOffsetTable.empty()) &&
+         "SecNameTable must be written before SecFuncOffsetTable to establish "
+         "Eytzinger indices!");
+
+  size_t SpanSize = IsCS ? NumCS : NumFlat;
+  size_t BaseIdx = IsCS ? 0 : NumCS;
+
+  std::vector<support::ulittle32_t> FuncOffsets(
+      SpanSize, support::ulittle32_t(UINT32_MAX));
+
+  // Populate the function offset array parallel to the Eytzinger span.
+  for (const auto &[Context, RelativeOffset] : FuncOffsetTable) {
+    if (RelativeOffset >= UINT32_MAX)
+      return sampleprof_error::too_large;
+
+    FunctionId FId = Context.getFunction();
+    auto It = NameTable.find(FId);
+    if (It == NameTable.end())
+      continue;
+
+    size_t GlobalIdx = It->second;
+    if (GlobalIdx < BaseIdx || (GlobalIdx - BaseIdx) >= SpanSize)
+      continue;
+
+    size_t LocalIdx = GlobalIdx - BaseIdx;
+    assert(
+        FuncOffsets[LocalIdx] == UINT32_MAX &&
+        "Function offset slot already populated; duplicate GUID or collision!");
+    FuncOffsets[LocalIdx] = static_cast<uint32_t>(RelativeOffset);
+  }
+
+  assert(!llvm::is_contained(FuncOffsets, support::ulittle32_t(UINT32_MAX)) &&
+         "Unpopulated slot in Eytzinger function offset array!");
+
+  OutputStream->write(reinterpret_cast<const char *>(FuncOffsets.data()),
+                      SpanSize * sizeof(support::ulittle32_t));
+  addSectionFlag(SecFuncOffsetTable, SecFuncOffsetFlags::SecFlagEytzinger);
+  FuncOffsetTable.clear();
+  return sampleprof_error::success;
+}
+
+std::error_code SampleProfileWriterExtBinaryBase::writeLegacyFuncOffsetTable() {
   auto &OS = *OutputStream;
 
   // Write out the table size.
@@ -464,6 +515,10 @@ class EytzingerNameTable {
       OS.write(reinterpret_cast<const char *>(Table.data()),
                Table.size() * sizeof(support::ulittle64_t));
   }
+
+  size_t size(EytzingerSpan S) const {
+    return Spans[static_cast<size_t>(S)].size();
+  }
 };
 
 } // end anonymous namespace
@@ -479,7 +534,9 @@ SampleProfileWriterExtBinaryBase::writeEytzingerNameTableSection(
     const SampleContext &Ctx = I.second.getContext();
     uint64_t GUID = Ctx.getFunction().getHashCode();
     if (TopLevelGUIDs.insert(GUID).second) {
-      if (I.second.isContextSensitiveTopLevel())
+      // In single-table default layouts, unify all top-level symbols in the CS
+      // partition so they match the single unflagged function offset table.
+      if (SecLayout != CtxSplitLayout || I.second.isContextSensitiveTopLevel())
         CSKeys.emplace_back(GUID);
       else
         FlatKeys.emplace_back(GUID);
@@ -502,6 +559,8 @@ SampleProfileWriterExtBinaryBase::writeEytzingerNameTableSection(
     Idx = Tables.findGlobalIdx(FId.getHashCode());
 
   Tables.write(*OutputStream);
+  NumCS = Tables.size(EytzingerSpan::CS);
+  NumFlat = Tables.size(EytzingerSpan::Flat);
 
   return sampleprof_error::success;
 }
@@ -606,10 +665,16 @@ std::error_code SampleProfileWriterExtBinaryBase::writeOneSection(
     if (std::error_code EC = writeFuncProfiles(ProfileMap))
       return EC;
     break;
-  case SecFuncOffsetTable:
-    if (auto EC = writeFuncOffsetTable())
+  case SecFuncOffsetTable: {
+    bool IsFlat =
+        hasSecFlag(SectionHdrLayout[LayoutIdx], SecCommonFlags::SecFlagFlat);
+    // An unflagged function offset table inherently indexes the primary
+    // context-sensitive symbol span.
+    bool IsCS = !IsFlat;
+    if (auto EC = writeFuncOffsetTable(IsCS))
       return EC;
     break;
+  }
   case SecFuncMetadata:
     if (std::error_code EC = writeFuncMetadata(ProfileMap))
       return EC;
diff --git a/llvm/test/tools/llvm-profdata/eytzinger-split-nametable-partition.test b/llvm/test/tools/llvm-profdata/eytzinger-split-nametable-partition.test
index f1b40d8c30694..0de8b06b59fb2 100644
--- a/llvm/test/tools/llvm-profdata/eytzinger-split-nametable-partition.test
+++ b/llvm/test/tools/llvm-profdata/eytzinger-split-nametable-partition.test
@@ -9,9 +9,9 @@
 
 # CHECK: ProfileSummarySection - Offset: {{.*}}, Size: {{.*}}, Flags: {}
 # CHECK: NameTableSection - Offset: {{.*}}, Size: {{.*}}, Flags: {eytzinger,fixlenmd5}
-# CHECK: FuncOffsetTableSection - Offset: {{.*}}, Size: {{.*}}, Flags: {}
+# CHECK: FuncOffsetTableSection - Offset: {{.*}}, Size: {{.*}}, Flags: {eytzinger}
 # CHECK: LBRProfileSection - Offset: {{.*}}, Size: {{.*}}, Flags: {}
-# CHECK: FuncOffsetTableSection - Offset: {{.*}}, Size: {{.*}}, Flags: {flat}
+# CHECK: FuncOffsetTableSection - Offset: {{.*}}, Size: {{.*}}, Flags: {flat,eytzinger}
 # CHECK: LBRProfileSection - Offset: {{.*}}, Size: {{.*}}, Flags: {flat}
 
 # COUNTS: [1, 1, 1]

>From 367f5cf7dbcb33cfb6232ec2f24bdabd32cf5b12 Mon Sep 17 00:00:00 2001
From: Kazu Hirata <kazu at google.com>
Date: Fri, 7 Aug 2026 14:21:13 -0700
Subject: [PATCH 2/3] Address comments.

---
 llvm/lib/ProfileData/SampleProfWriter.cpp            | 10 +++++++++-
 llvm/test/tools/llvm-profdata/cs-sample-profile.test |  3 +++
 2 files changed, 12 insertions(+), 1 deletion(-)

diff --git a/llvm/lib/ProfileData/SampleProfWriter.cpp b/llvm/lib/ProfileData/SampleProfWriter.cpp
index 4d87f376702ae..22c63b660a4ce 100644
--- a/llvm/lib/ProfileData/SampleProfWriter.cpp
+++ b/llvm/lib/ProfileData/SampleProfWriter.cpp
@@ -280,8 +280,13 @@ SampleProfileWriterExtBinaryBase::writeSample(const FunctionSamples &S) {
 
 std::error_code
 SampleProfileWriterExtBinaryBase::writeFuncOffsetTable(bool IsCS) {
-  if (WriteEytzingerNameTables)
+  if (WriteEytzingerNameTables) {
+    // Eytzinger layout requires MD5 representation and does not support
+    // multi-context Context-Sensitive profiles.
+    if (!UseMD5 || FunctionSamples::ProfileIsCS)
+      return sampleprof_error::unsupported_writing_format;
     return writeEytzingerFuncOffsetTable(IsCS);
+  }
   return writeLegacyFuncOffsetTable();
 }
 
@@ -460,6 +465,9 @@ std::error_code SampleProfileWriterExtBinaryBase::writeNameTableSection(
   }
 
   if (UseMD5 && WriteEytzingerNameTables) {
+    // Eytzinger name tables do not support Context-Sensitive profiles.
+    if (FunctionSamples::ProfileIsCS)
+      return sampleprof_error::unsupported_writing_format;
     if (auto EC = writeEytzingerNameTableSection(ProfileMap))
       return EC;
     return sampleprof_error::success;
diff --git a/llvm/test/tools/llvm-profdata/cs-sample-profile.test b/llvm/test/tools/llvm-profdata/cs-sample-profile.test
index ce69a1ffd61a6..aaafca2bfe542 100644
--- a/llvm/test/tools/llvm-profdata/cs-sample-profile.test
+++ b/llvm/test/tools/llvm-profdata/cs-sample-profile.test
@@ -4,3 +4,6 @@ RUN: llvm-profdata merge --sample --extbinary %p/Inputs/cs-sample.proftext -o %t
 RUN: diff -b %t1.proftext %S/Inputs/cs-sample.proftext
 RUN: llvm-profdata show --sample -show-sec-info-only %t.prof | FileCheck %s
 CHECK: FunctionMetadata {{.*}} Flags: {attr}
+
+RUN: not llvm-profdata merge --sample --extbinary --use-md5 --sample-profile-write-eytzinger-name-tables %p/Inputs/cs-sample.proftext -o /dev/null 2>&1 | FileCheck %s --check-prefix=ERR-CS-EYTZ
+ERR-CS-EYTZ: error: {{.*}}Profile encoding format unsupported for writing operations

>From c30b6dbd1edf0b3fefe36d808be1cb9f685ba1e3 Mon Sep 17 00:00:00 2001
From: Kazu Hirata <kazu at google.com>
Date: Sun, 9 Aug 2026 23:44:43 -0700
Subject: [PATCH 3/3] Address comments.

---
 llvm/include/llvm/ProfileData/SampleProfReader.h | 16 +++++++++++-----
 llvm/lib/ProfileData/SampleProfWriter.cpp        |  3 ++-
 .../eytzinger-split-nametable-partition.test     |  9 +++++++++
 3 files changed, 22 insertions(+), 6 deletions(-)

diff --git a/llvm/include/llvm/ProfileData/SampleProfReader.h b/llvm/include/llvm/ProfileData/SampleProfReader.h
index 6eb5760d8e1e6..e0d140c0c8d2c 100644
--- a/llvm/include/llvm/ProfileData/SampleProfReader.h
+++ b/llvm/include/llvm/ProfileData/SampleProfReader.h
@@ -1042,6 +1042,8 @@ inline constexpr EytzingerModeT EytzingerMode{};
 /// modification in on-disk mode.
 class SampleProfileFuncOffsetTable {
 public:
+  enum class TableMode { InMemory, OnDisk, Eytzinger };
+
   using OnDiskTableType =
       llvm::OnDiskIterableChainedHashTable<FuncOffsetHashTableInfo>;
 
@@ -1054,14 +1056,16 @@ class SampleProfileFuncOffsetTable {
   operator=(SampleProfileFuncOffsetTable &&) = delete;
 
   explicit SampleProfileFuncOffsetTable(InMemoryModeT,
-                                        size_t InitialCapacity = 0) {
+                                        size_t InitialCapacity = 0)
+      : Mode(TableMode::InMemory) {
     InMemoryTable.reserve(InitialCapacity);
   }
 
   SampleProfileFuncOffsetTable(
       EytzingerModeT, EytzingerTableSpan<support::ulittle64_t> NameSpan,
       ArrayRef<support::ulittle32_t> FuncOffsetSpan)
-      : NameSpan(NameSpan), FuncOffsetSpan(FuncOffsetSpan) {}
+      : Mode(TableMode::Eytzinger), NameSpan(NameSpan),
+        FuncOffsetSpan(FuncOffsetSpan) {}
 
   /// Insert a function GUID and its profile offset into the in-memory map.
   /// Enforces that the on-disk table must not have been set first.
@@ -1073,7 +1077,8 @@ class SampleProfileFuncOffsetTable {
 
   /// Instantiate the on-disk chained hash table using raw stream pointers.
   SampleProfileFuncOffsetTable(OnDiskModeT, const uint8_t *Buckets,
-                               const uint8_t *Payload, const uint8_t *Base) {
+                               const uint8_t *Payload, const uint8_t *Base)
+      : Mode(TableMode::OnDisk) {
     OnDiskTable.reset(OnDiskTableType::Create(Buckets, Payload, Base));
   }
 
@@ -1101,7 +1106,7 @@ class SampleProfileFuncOffsetTable {
   }
 
   /// Direct read-only array (`ArrayRef`) of function offsets aligned parallel
-  /// to the corresponding Eytzinger name span:
+  /// to the corresponding Eytzinger name span.
   ArrayRef<support::ulittle32_t> getFuncOffsets() const {
     assert(isEytzinger() &&
            "Cannot call getFuncOffsets() on non-Eytzinger table");
@@ -1114,9 +1119,10 @@ class SampleProfileFuncOffsetTable {
     return NameSpan.size();
   }
 
-  bool isEytzinger() const { return FuncOffsetSpan.data() != nullptr; }
+  bool isEytzinger() const { return Mode == TableMode::Eytzinger; }
 
 private:
+  TableMode Mode;
   llvm::DenseMap<hash_code, uint64_t> InMemoryTable;
   std::unique_ptr<OnDiskTableType> OnDiskTable;
   EytzingerTableSpan<support::ulittle64_t> NameSpan;
diff --git a/llvm/lib/ProfileData/SampleProfWriter.cpp b/llvm/lib/ProfileData/SampleProfWriter.cpp
index 22c63b660a4ce..1962c83e6e7e6 100644
--- a/llvm/lib/ProfileData/SampleProfWriter.cpp
+++ b/llvm/lib/ProfileData/SampleProfWriter.cpp
@@ -465,7 +465,8 @@ std::error_code SampleProfileWriterExtBinaryBase::writeNameTableSection(
   }
 
   if (UseMD5 && WriteEytzingerNameTables) {
-    // Eytzinger name tables do not support Context-Sensitive profiles.
+    // Eytzinger name tables do not support CSSPGO profiles
+    // (FunctionSamples::ProfileIsCS).
     if (FunctionSamples::ProfileIsCS)
       return sampleprof_error::unsupported_writing_format;
     if (auto EC = writeEytzingerNameTableSection(ProfileMap))
diff --git a/llvm/test/tools/llvm-profdata/eytzinger-split-nametable-partition.test b/llvm/test/tools/llvm-profdata/eytzinger-split-nametable-partition.test
index 0de8b06b59fb2..e27fd8ed6081c 100644
--- a/llvm/test/tools/llvm-profdata/eytzinger-split-nametable-partition.test
+++ b/llvm/test/tools/llvm-profdata/eytzinger-split-nametable-partition.test
@@ -2,6 +2,8 @@
 # RUN:   --sample-profile-write-eytzinger-name-tables %s -o %t.xfdo
 # RUN: llvm-profdata show --sample --show-sec-info-only %t.xfdo > %t.secinfo
 # RUN: FileCheck %s -input-file %t.secinfo
+# RUN: llvm-profdata merge --sample --text %t.xfdo -o %t.text
+# RUN: FileCheck %s --check-prefix=TEXT -input-file %t.text
 # Extract and verify the Eytzinger span counts (NumCS, NumFlat,
 # NumInlinees) from NameTableSection to ensure correct symbol
 # partitioning.
@@ -16,6 +18,13 @@
 
 # COUNTS: [1, 1, 1]
 
+# TEXT: 15822663052811949562:1000:10
+# TEXT:  1: 1000
+# TEXT:  2: 6699318081062747564:500
+# TEXT:   1: 500
+# TEXT: 110832587303349897:100:5
+# TEXT:  1: 100
+
 main:1000:10
  1: 1000
  2: foo:500



More information about the llvm-commits mailing list