[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