[llvm] [ADT] Introduce EytzingerTableSpan (PR #208885)

Kazu Hirata via llvm-commits llvm-commits at lists.llvm.org
Sat Jul 11 17:08:24 PDT 2026


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

>From 42fbd67424457e74cf52ee608e9d8df7235f350b Mon Sep 17 00:00:00 2001
From: Kazu Hirata <kazu at google.com>
Date: Fri, 10 Jul 2026 20:49:42 -0700
Subject: [PATCH 1/3] [ADT] Introduce EytzingerTableSpan

This patch introduces EytzingerTableSpan, a non-owning view of a buffer
formatted as a complete binary search tree in Eytzinger (breadth-first)
order.

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

Assisted-by: Antigravity
---
 llvm/include/llvm/ADT/Eytzinger.h    |  68 ++++++++++++++
 llvm/unittests/ADT/CMakeLists.txt    |   1 +
 llvm/unittests/ADT/EytzingerTest.cpp | 134 +++++++++++++++++++++++++++
 3 files changed, 203 insertions(+)
 create mode 100644 llvm/include/llvm/ADT/Eytzinger.h
 create mode 100644 llvm/unittests/ADT/EytzingerTest.cpp

diff --git a/llvm/include/llvm/ADT/Eytzinger.h b/llvm/include/llvm/ADT/Eytzinger.h
new file mode 100644
index 0000000000000..1b13f076e1a27
--- /dev/null
+++ b/llvm/include/llvm/ADT/Eytzinger.h
@@ -0,0 +1,68 @@
+//===- Eytzinger.h - Eytzinger Search Tree Span -----------------*- C++ -*-===//
+//
+// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
+// See https://llvm.org/LICENSE.txt for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+//===----------------------------------------------------------------------===//
+///
+/// \file
+/// This file defines the EytzingerTableSpan class, a non-owning view of a
+/// buffer formatted as a complete binary search tree in Eytzinger
+/// (breadth-first) order.
+///
+//===----------------------------------------------------------------------===//
+
+#ifndef LLVM_ADT_EYTZINGER_H
+#define LLVM_ADT_EYTZINGER_H
+
+#include "llvm/ADT/bit.h"
+#include <cassert>
+#include <cstddef>
+#include <optional>
+
+namespace llvm {
+
+/// Non-owning view of a buffer formatted as a complete binary search tree in
+/// Eytzinger (breadth-first) order.
+template <typename T> class EytzingerTableSpan {
+public:
+  EytzingerTableSpan() = default;
+  EytzingerTableSpan(const T *Data, size_t NumEntries)
+      : Data(Data), NumEntries(NumEntries) {}
+
+  [[nodiscard]] const T *data() const { return Data; }
+  [[nodiscard]] bool empty() const { return !Data || NumEntries == 0; }
+  [[nodiscard]] size_t size() const { return NumEntries; }
+  [[nodiscard]] const T &operator[](size_t Idx) const {
+    assert(Idx < NumEntries && "Index out of bounds");
+    return Data[Idx];
+  }
+
+  /// Search this Eytzinger table for Target using branchless binary search.
+  /// Returns the 0-based array index if found.
+  ///
+  /// KeyT enables heterogeneous lookups, allowing callers to search tables of
+  /// endian-specific wrappers (e.g., support::ulittle64_t) using native integer
+  /// keys without explicit conversions at the call site.
+  template <typename KeyT = T>
+  [[nodiscard]] std::optional<size_t> findIndex(const KeyT &Target) const {
+    if (empty())
+      return std::nullopt;
+    size_t K = 1;
+    while (K <= NumEntries)
+      K = 2 * K + (Data[K - 1] < Target);
+    K >>= llvm::countr_one(K) + 1;
+    if (K >= 1 && Data[K - 1] == Target)
+      return K - 1;
+    return std::nullopt;
+  }
+
+private:
+  const T *Data = nullptr;
+  size_t NumEntries = 0;
+};
+
+} // namespace llvm
+
+#endif // LLVM_ADT_EYTZINGER_H
diff --git a/llvm/unittests/ADT/CMakeLists.txt b/llvm/unittests/ADT/CMakeLists.txt
index 3e0b46e753340..2d129212ead02 100644
--- a/llvm/unittests/ADT/CMakeLists.txt
+++ b/llvm/unittests/ADT/CMakeLists.txt
@@ -30,6 +30,7 @@ add_llvm_unittest(ADTTests
   EditDistanceTest.cpp
   EnumeratedArrayTest.cpp
   EquivalenceClassesTest.cpp
+  EytzingerTest.cpp
   FallibleIteratorTest.cpp
   FloatingPointMode.cpp
   FoldingSet.cpp
diff --git a/llvm/unittests/ADT/EytzingerTest.cpp b/llvm/unittests/ADT/EytzingerTest.cpp
new file mode 100644
index 0000000000000..a409c35dfb672
--- /dev/null
+++ b/llvm/unittests/ADT/EytzingerTest.cpp
@@ -0,0 +1,134 @@
+//===- EytzingerTest.cpp - EytzingerTableSpan unit tests ------------------===//
+//
+// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
+// See https://llvm.org/LICENSE.txt for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+//===----------------------------------------------------------------------===//
+
+#include "llvm/ADT/Eytzinger.h"
+#include "llvm/Support/Endian.h"
+#include "gtest/gtest.h"
+
+using namespace llvm;
+
+namespace {
+
+TEST(EytzingerTest, EmptyTable) {
+  // Default constructed table span should be empty and return nullopt.
+  EytzingerTableSpan<int> Empty;
+  EXPECT_TRUE(Empty.empty());
+  EXPECT_EQ(Empty.size(), 0u);
+  EXPECT_EQ(Empty.data(), nullptr);
+  EXPECT_EQ(Empty.findIndex(42), std::nullopt);
+
+  // Span initialized with nullptr and zero size should behave identically.
+  EytzingerTableSpan<int> NullSpan(nullptr, 0);
+  EXPECT_TRUE(NullSpan.empty());
+  EXPECT_EQ(NullSpan.size(), 0u);
+  EXPECT_EQ(NullSpan.findIndex(42), std::nullopt);
+}
+
+TEST(EytzingerTest, SingleElementTable) {
+  // Table with a single element at index 0.
+  const int Data[] = {100};
+  EytzingerTableSpan<int> Span(Data, 1);
+
+  EXPECT_FALSE(Span.empty());
+  EXPECT_EQ(Span.size(), 1u);
+  EXPECT_EQ(Span[0], 100);
+
+  // Successful lookup for the single element.
+  EXPECT_EQ(Span.findIndex(100), 0u);
+
+  // Unsuccessful lookups for keys smaller and larger than the element.
+  EXPECT_EQ(Span.findIndex(99), std::nullopt);
+  EXPECT_EQ(Span.findIndex(101), std::nullopt);
+}
+
+TEST(EytzingerTest, BinaryTreeWithSevenElements) {
+  // Binary tree of 7 elements (3 full levels).
+  // In Eytzinger layout (breadth-first order of complete BST):
+  //
+  //         40
+  //       /    \
+  //     20      60
+  //    /  \    /  \
+  //  10   30  50   70
+  const int Data[] = {40, 20, 60, 10, 30, 50, 70};
+  EytzingerTableSpan<int> Span(Data, 7);
+
+  EXPECT_EQ(Span.size(), 7u);
+
+  // Verify successful lookups for every node in the tree.
+  EXPECT_EQ(Span.findIndex(40), 0u);
+  EXPECT_EQ(Span.findIndex(20), 1u);
+  EXPECT_EQ(Span.findIndex(60), 2u);
+  EXPECT_EQ(Span.findIndex(10), 3u);
+  EXPECT_EQ(Span.findIndex(30), 4u);
+  EXPECT_EQ(Span.findIndex(50), 5u);
+  EXPECT_EQ(Span.findIndex(70), 6u);
+
+  // Verify unsuccessful lookups for values not in the tree.
+  EXPECT_EQ(Span.findIndex(0), std::nullopt);
+  EXPECT_EQ(Span.findIndex(15), std::nullopt);
+  EXPECT_EQ(Span.findIndex(25), std::nullopt);
+  EXPECT_EQ(Span.findIndex(35), std::nullopt);
+  EXPECT_EQ(Span.findIndex(45), std::nullopt);
+  EXPECT_EQ(Span.findIndex(55), std::nullopt);
+  EXPECT_EQ(Span.findIndex(65), std::nullopt);
+  EXPECT_EQ(Span.findIndex(80), std::nullopt);
+}
+
+TEST(EytzingerTest, BinaryTreeWithFiveElements) {
+  // Binary tree of 5 elements (non-power-of-two minus one).
+  // In Eytzinger layout:
+  //
+  //       40
+  //      /  \
+  //    20    50
+  //   /  \
+  // 10    30
+  const int Data[] = {40, 20, 50, 10, 30};
+  EytzingerTableSpan<int> Span(Data, 5);
+
+  EXPECT_EQ(Span.size(), 5u);
+
+  // Verify lookups on existing elements.
+  EXPECT_EQ(Span.findIndex(40), 0u);
+  EXPECT_EQ(Span.findIndex(20), 1u);
+  EXPECT_EQ(Span.findIndex(50), 2u);
+  EXPECT_EQ(Span.findIndex(10), 3u);
+  EXPECT_EQ(Span.findIndex(30), 4u);
+
+  // Verify lookups on missing values across various boundary conditions.
+  EXPECT_EQ(Span.findIndex(5), std::nullopt);
+  EXPECT_EQ(Span.findIndex(15), std::nullopt);
+  EXPECT_EQ(Span.findIndex(25), std::nullopt);
+  EXPECT_EQ(Span.findIndex(35), std::nullopt);
+  EXPECT_EQ(Span.findIndex(45), std::nullopt);
+  EXPECT_EQ(Span.findIndex(60), std::nullopt);
+}
+
+TEST(EytzingerTest, EndianSpecificIntegerType) {
+  // Verify compatibility with LLVM endian-specific wrapper types such as
+  // support::ulittle64_t which are commonly used in binary profile formats.
+  const support::ulittle64_t Data[] = {
+      support::ulittle64_t(400), support::ulittle64_t(200),
+      support::ulittle64_t(600), support::ulittle64_t(100),
+      support::ulittle64_t(300), support::ulittle64_t(500),
+      support::ulittle64_t(700)};
+  EytzingerTableSpan<support::ulittle64_t> Span(Data, 7);
+
+  EXPECT_EQ(Span.findIndex(uint64_t(400)), 0u);
+  EXPECT_EQ(Span.findIndex(uint64_t(200)), 1u);
+  EXPECT_EQ(Span.findIndex(uint64_t(600)), 2u);
+  EXPECT_EQ(Span.findIndex(uint64_t(100)), 3u);
+  EXPECT_EQ(Span.findIndex(uint64_t(300)), 4u);
+  EXPECT_EQ(Span.findIndex(uint64_t(500)), 5u);
+  EXPECT_EQ(Span.findIndex(uint64_t(700)), 6u);
+
+  EXPECT_EQ(Span.findIndex(uint64_t(999)), std::nullopt);
+}
+
+} // namespace

>From 6ea73ac3fd13bf792ba953adc7215ca6311a26ee Mon Sep 17 00:00:00 2001
From: Kazu Hirata <kazu at google.com>
Date: Sat, 11 Jul 2026 16:16:10 -0700
Subject: [PATCH 2/3] Address comments.

---
 llvm/include/llvm/ADT/Eytzinger.h    | 63 +++++++++++++++++++++++-----
 llvm/unittests/ADT/EytzingerTest.cpp | 24 +++++++++++
 2 files changed, 76 insertions(+), 11 deletions(-)

diff --git a/llvm/include/llvm/ADT/Eytzinger.h b/llvm/include/llvm/ADT/Eytzinger.h
index 1b13f076e1a27..13d6db1f35621 100644
--- a/llvm/include/llvm/ADT/Eytzinger.h
+++ b/llvm/include/llvm/ADT/Eytzinger.h
@@ -16,7 +16,6 @@
 #ifndef LLVM_ADT_EYTZINGER_H
 #define LLVM_ADT_EYTZINGER_H
 
-#include "llvm/ADT/bit.h"
 #include <cassert>
 #include <cstddef>
 #include <optional>
@@ -39,25 +38,67 @@ template <typename T> class EytzingerTableSpan {
     return Data[Idx];
   }
 
-  /// Search this Eytzinger table for Target using branchless binary search.
-  /// Returns the 0-based array index if found.
+  /// Search this Eytzinger table for Target. Returns the 0-based array index if
+  /// found.
   ///
   /// KeyT enables heterogeneous lookups, allowing callers to search tables of
   /// endian-specific wrappers (e.g., support::ulittle64_t) using native integer
   /// keys without explicit conversions at the call site.
   template <typename KeyT = T>
   [[nodiscard]] std::optional<size_t> findIndex(const KeyT &Target) const {
-    if (empty())
-      return std::nullopt;
-    size_t K = 1;
-    while (K <= NumEntries)
-      K = 2 * K + (Data[K - 1] < Target);
-    K >>= llvm::countr_one(K) + 1;
-    if (K >= 1 && Data[K - 1] == Target)
-      return K - 1;
+    size_t I = 0;
+    while (I < NumEntries) {
+      if (Data[I] == Target)
+        return I;
+      I = 2 * I + 1 + (Data[I] < Target);
+    }
     return std::nullopt;
   }
 
+  /// Verify whether the buffer satisfies strictly ascending binary search tree
+  /// order in Eytzinger layout. Runs iteratively in O(N) time and O(1) space.
+  [[nodiscard]] bool isSorted() const {
+    if (empty())
+      return true;
+
+    auto Left = [](size_t I) { return 2 * I + 1; };
+    auto Right = [](size_t I) { return 2 * I + 2; };
+    auto Parent = [](size_t I) { return (I - 1) / 2; };
+    auto IsRightChild = [](size_t I) { return I > 0 && I % 2 == 0; };
+    auto HasLeft = [&](size_t I) { return Left(I) < NumEntries; };
+    auto HasRight = [&](size_t I) { return Right(I) < NumEntries; };
+
+    // Start at the leftmost leaf (in-order minimum).
+    size_t Curr = 0;
+    while (HasLeft(Curr))
+      Curr = Left(Curr);
+
+    const T *Prev = nullptr;
+    while (Curr < NumEntries) {
+      if (Prev && !(*Prev < Data[Curr]))
+        return false;
+      Prev = &Data[Curr];
+
+      // Step to the in-order successor of Curr.
+      if (HasRight(Curr)) {
+        // If Curr has a right subtree, successor is its leftmost leaf.
+        Curr = Right(Curr);
+        while (HasLeft(Curr))
+          Curr = Left(Curr);
+      } else {
+        // Otherwise, walk upward while we are in the right branch.
+        while (IsRightChild(Curr))
+          Curr = Parent(Curr);
+        // Traversed the entire tree; done.
+        if (Curr == 0)
+          break;
+        // Step up from left child to parent.
+        Curr = Parent(Curr);
+      }
+    }
+    return true;
+  }
+
 private:
   const T *Data = nullptr;
   size_t NumEntries = 0;
diff --git a/llvm/unittests/ADT/EytzingerTest.cpp b/llvm/unittests/ADT/EytzingerTest.cpp
index a409c35dfb672..d71e1589f6516 100644
--- a/llvm/unittests/ADT/EytzingerTest.cpp
+++ b/llvm/unittests/ADT/EytzingerTest.cpp
@@ -20,12 +20,14 @@ TEST(EytzingerTest, EmptyTable) {
   EXPECT_TRUE(Empty.empty());
   EXPECT_EQ(Empty.size(), 0u);
   EXPECT_EQ(Empty.data(), nullptr);
+  EXPECT_TRUE(Empty.isSorted());
   EXPECT_EQ(Empty.findIndex(42), std::nullopt);
 
   // Span initialized with nullptr and zero size should behave identically.
   EytzingerTableSpan<int> NullSpan(nullptr, 0);
   EXPECT_TRUE(NullSpan.empty());
   EXPECT_EQ(NullSpan.size(), 0u);
+  EXPECT_TRUE(NullSpan.isSorted());
   EXPECT_EQ(NullSpan.findIndex(42), std::nullopt);
 }
 
@@ -36,6 +38,7 @@ TEST(EytzingerTest, SingleElementTable) {
 
   EXPECT_FALSE(Span.empty());
   EXPECT_EQ(Span.size(), 1u);
+  EXPECT_TRUE(Span.isSorted());
   EXPECT_EQ(Span[0], 100);
 
   // Successful lookup for the single element.
@@ -59,6 +62,7 @@ TEST(EytzingerTest, BinaryTreeWithSevenElements) {
   EytzingerTableSpan<int> Span(Data, 7);
 
   EXPECT_EQ(Span.size(), 7u);
+  EXPECT_TRUE(Span.isSorted());
 
   // Verify successful lookups for every node in the tree.
   EXPECT_EQ(Span.findIndex(40), 0u);
@@ -93,6 +97,7 @@ TEST(EytzingerTest, BinaryTreeWithFiveElements) {
   EytzingerTableSpan<int> Span(Data, 5);
 
   EXPECT_EQ(Span.size(), 5u);
+  EXPECT_TRUE(Span.isSorted());
 
   // Verify lookups on existing elements.
   EXPECT_EQ(Span.findIndex(40), 0u);
@@ -119,6 +124,7 @@ TEST(EytzingerTest, EndianSpecificIntegerType) {
       support::ulittle64_t(300), support::ulittle64_t(500),
       support::ulittle64_t(700)};
   EytzingerTableSpan<support::ulittle64_t> Span(Data, 7);
+  EXPECT_TRUE(Span.isSorted());
 
   EXPECT_EQ(Span.findIndex(uint64_t(400)), 0u);
   EXPECT_EQ(Span.findIndex(uint64_t(200)), 1u);
@@ -131,4 +137,22 @@ TEST(EytzingerTest, EndianSpecificIntegerType) {
   EXPECT_EQ(Span.findIndex(uint64_t(999)), std::nullopt);
 }
 
+TEST(EytzingerTest, IsSortedVerification) {
+  // Verify detection of local parent-child violations.
+  // Root (40), left child (60 > 40), right child (20 < 40).
+  const int InvalidChildOrder[] = {40, 60, 20};
+  EXPECT_FALSE(EytzingerTableSpan<int>(InvalidChildOrder, 3).isSorted());
+
+  // Verify detection of across-level ancestor bounds violations.
+  // Root (40), left (20), right (60). Left of 20 is 10, right of 20 is 50.
+  // Although 50 > 20 (local parent check passes), 50 > 40 violates the root bound.
+  const int AncestorViolation[] = {40, 20, 60, 10, 50, 55, 70};
+  EXPECT_FALSE(EytzingerTableSpan<int>(AncestorViolation, 7).isSorted());
+
+  // Verify that tables with duplicate values are flagged as unsorted because
+  // EytzingerTableSpan requires strictly ascending in-order keys.
+  const int Duplicates[] = {30, 30, 30};
+  EXPECT_FALSE(EytzingerTableSpan<int>(Duplicates, 3).isSorted());
+}
+
 } // namespace

>From 7971e2e32d03f05e3423a1f7e6aaa4d43af7aab0 Mon Sep 17 00:00:00 2001
From: Kazu Hirata <kazu at google.com>
Date: Sat, 11 Jul 2026 17:08:01 -0700
Subject: [PATCH 3/3] Fix formatting.

---
 llvm/unittests/ADT/EytzingerTest.cpp | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/llvm/unittests/ADT/EytzingerTest.cpp b/llvm/unittests/ADT/EytzingerTest.cpp
index d71e1589f6516..1bb1b06225467 100644
--- a/llvm/unittests/ADT/EytzingerTest.cpp
+++ b/llvm/unittests/ADT/EytzingerTest.cpp
@@ -145,7 +145,8 @@ TEST(EytzingerTest, IsSortedVerification) {
 
   // Verify detection of across-level ancestor bounds violations.
   // Root (40), left (20), right (60). Left of 20 is 10, right of 20 is 50.
-  // Although 50 > 20 (local parent check passes), 50 > 40 violates the root bound.
+  // Although 50 > 20 (local parent check passes), 50 > 40 violates the root
+  // bound.
   const int AncestorViolation[] = {40, 20, 60, 10, 50, 55, 70};
   EXPECT_FALSE(EytzingerTableSpan<int>(AncestorViolation, 7).isSorted());
 



More information about the llvm-commits mailing list