mirror of
https://github.com/fmtlib/fmt.git
synced 2026-09-13 14:22:26 +08:00
Look up sparse enum identifiers in a hash table instead of a linear search (#4899)
Enums that are too sparse for the index table were formatted by scanning all enumerators. Replace the scan with an open-addressed hash table with linear probing, sized to the smallest power of two that keeps the load factor at or below 0.5. This guarantees a free slot, which terminates the probe sequence and doubles as the not-found result because identifiers are never empty. Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
e76a9520a3
commit
e589a16ecb
@ -124,17 +124,50 @@ consteval auto make_identifier_table() -> std::array<string_view, N> {
|
||||
template <typename E>
|
||||
inline constexpr auto identifier_table = make_identifier_table<E>();
|
||||
|
||||
template <typename E, size_t N = count_enumerators<E>()>
|
||||
consteval auto make_identifiers() -> std::array<std::pair<E, string_view>, N> {
|
||||
auto ids = std::array<std::pair<E, string_view>, N>();
|
||||
auto i = size_t();
|
||||
for (std::meta::info e : std::meta::enumerators_of(^^E))
|
||||
ids[i++] = {std::meta::extract<E>(e), identifier(e)};
|
||||
return ids;
|
||||
// Returns the size of the hash table that maps enumerator values of E to
|
||||
// identifiers. It is the smallest power of two that keeps the load factor at or
|
||||
// below 0.5, which guarantees a free slot and therefore terminates the search.
|
||||
template <typename E> consteval auto identifier_map_size() -> size_t {
|
||||
auto size = size_t(1);
|
||||
while (size < count_enumerators<E>() * 2) size *= 2;
|
||||
return size;
|
||||
}
|
||||
|
||||
// Identifiers of enumerators of E in the order of declaration.
|
||||
template <typename E> inline constexpr auto identifiers = make_identifiers<E>();
|
||||
// Returns the index of the first slot to probe for `value`. The bits of the
|
||||
// underlying value are mixed because enumerator values are usually small and
|
||||
// only the low bits of the hash are used.
|
||||
template <typename E, size_t N = identifier_map_size<E>()>
|
||||
constexpr auto identifier_slot(E value) -> size_t {
|
||||
auto h = to_uint64(value);
|
||||
h ^= h >> 33;
|
||||
h *= 0xff51afd7ed558ccd;
|
||||
h ^= h >> 33;
|
||||
return static_cast<size_t>(h & (N - 1));
|
||||
}
|
||||
|
||||
template <typename E, size_t N = identifier_map_size<E>()>
|
||||
consteval auto make_identifier_map()
|
||||
-> std::array<std::pair<E, string_view>, N> {
|
||||
auto map = std::array<std::pair<E, string_view>, N>();
|
||||
for (std::meta::info e : std::meta::enumerators_of(^^E)) {
|
||||
auto value = std::meta::extract<E>(e);
|
||||
for (auto i = identifier_slot(value);; i = (i + 1) & (N - 1)) {
|
||||
if (map[i].second.size() != 0) {
|
||||
// Keep the identifier of the first enumerator with this value.
|
||||
if (map[i].first == value) break;
|
||||
continue; // The slot is taken by another value; probe the next one.
|
||||
}
|
||||
map[i] = {value, identifier(e)};
|
||||
break;
|
||||
}
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
// Identifiers of enumerators of E in an open-addressed hash table with linear
|
||||
// probing and empty string views in the free slots.
|
||||
template <typename E>
|
||||
inline constexpr auto identifier_map = make_identifier_map<E>();
|
||||
|
||||
// Returns the identifier of the first enumerator of E equal to value or an
|
||||
// empty string view if there is no such enumerator.
|
||||
@ -146,10 +179,12 @@ template <typename E> constexpr auto identifier_of(E value) -> string_view {
|
||||
return i < table_size ? identifier_table<E>[static_cast<size_t>(i)]
|
||||
: string_view();
|
||||
} else {
|
||||
for (const auto& id : identifiers<E>) {
|
||||
if (id.first == value) return id.second;
|
||||
constexpr size_t map_size = identifier_map_size<E>();
|
||||
// A free slot terminates the search and its empty identifier is the result.
|
||||
for (auto i = identifier_slot(value);; i = (i + 1) & (map_size - 1)) {
|
||||
const auto& entry = identifier_map<E>[i];
|
||||
if (entry.second.size() == 0 || entry.first == value) return entry.second;
|
||||
}
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -37,7 +37,7 @@ enum class [[=fmt::as_identifiers]] dense { d0, d1, d2, d3, d4 };
|
||||
enum class [[=fmt::as_identifiers]] holey {
|
||||
h0, h1, h2, h3, h4, h5, h6 = 9
|
||||
};
|
||||
// 4 holes out of 11: just too sparse, formatted via a linear search.
|
||||
// 4 holes out of 11: just too sparse, formatted via a hash table.
|
||||
enum class [[=fmt::as_identifiers]] sparse {
|
||||
s0, s1, s2, s3, s4, s5, s6 = 10
|
||||
};
|
||||
@ -47,6 +47,25 @@ enum class [[=fmt::as_identifiers]] signed_enum {
|
||||
minus_one = -1,
|
||||
one = 1
|
||||
};
|
||||
// Many scattered values, exercising collisions in the hash table.
|
||||
enum class [[=fmt::as_identifiers]] scattered {
|
||||
a = 1, b = 17, c = 33, d = 49, e = 65, f = 81,
|
||||
g = 97, h = 113, i = 129, j = 145, k = 161, l = 177
|
||||
};
|
||||
// Values that collide in the hash table: c0, c7 and c15 share a slot, and c6
|
||||
// occupies the next one, so probing for c7 and c15 has to step over it.
|
||||
enum class [[=fmt::as_identifiers]] collision {
|
||||
c0 = 0, c6 = 6, c7 = 7, c15 = 15
|
||||
};
|
||||
// Values that collide in the last slot of the hash table, so the probe
|
||||
// sequence wraps around to the beginning.
|
||||
enum class [[=fmt::as_identifiers]] wrapping_collision {
|
||||
w8 = 8, w16 = 16, w24 = 24
|
||||
};
|
||||
// Aliased values in an enum that is too sparse for a lookup table.
|
||||
enum class [[=fmt::as_identifiers]] sparse_alias {
|
||||
one = 1, dup = 1, far = 1000
|
||||
};
|
||||
enum class [[=fmt::as_identifiers]] extremes : int {
|
||||
lowest = INT_MIN,
|
||||
highest = INT_MAX
|
||||
@ -97,6 +116,8 @@ TEST(enum_test, format_holey_enum) {
|
||||
TEST(enum_test, format_sparse_enum) {
|
||||
// One more hole than holey, which is too many for a lookup table.
|
||||
static_assert(fmt::detail::identifier_table_size<sparse>() == 0);
|
||||
// 7 enumerators need 16 slots to keep the load factor at or below 0.5.
|
||||
static_assert(fmt::detail::identifier_map_size<sparse>() == 16);
|
||||
EXPECT_EQ(fmt::format("{}", sparse::s0), "s0");
|
||||
EXPECT_EQ(fmt::format("{}", sparse::s5), "s5");
|
||||
EXPECT_EQ(fmt::format("{}", sparse::s6), "s6");
|
||||
@ -115,6 +136,68 @@ TEST(enum_test, format_enum_with_negative_values) {
|
||||
EXPECT_EQ(fmt::format("{}", static_cast<signed_enum>(2)), "2");
|
||||
}
|
||||
|
||||
TEST(enum_test, format_scattered_enum) {
|
||||
static_assert(fmt::detail::identifier_table_size<scattered>() == 0);
|
||||
static_assert(fmt::detail::identifier_map_size<scattered>() == 32);
|
||||
EXPECT_EQ(fmt::format("{}", scattered::a), "a");
|
||||
EXPECT_EQ(fmt::format("{}", scattered::b), "b");
|
||||
EXPECT_EQ(fmt::format("{}", scattered::c), "c");
|
||||
EXPECT_EQ(fmt::format("{}", scattered::d), "d");
|
||||
EXPECT_EQ(fmt::format("{}", scattered::e), "e");
|
||||
EXPECT_EQ(fmt::format("{}", scattered::f), "f");
|
||||
EXPECT_EQ(fmt::format("{}", scattered::g), "g");
|
||||
EXPECT_EQ(fmt::format("{}", scattered::h), "h");
|
||||
EXPECT_EQ(fmt::format("{}", scattered::i), "i");
|
||||
EXPECT_EQ(fmt::format("{}", scattered::j), "j");
|
||||
EXPECT_EQ(fmt::format("{}", scattered::k), "k");
|
||||
EXPECT_EQ(fmt::format("{}", scattered::l), "l");
|
||||
EXPECT_EQ(fmt::format("{}", static_cast<scattered>(0)), "0");
|
||||
EXPECT_EQ(fmt::format("{}", static_cast<scattered>(-1)), "-1");
|
||||
EXPECT_EQ(fmt::format("{}", static_cast<scattered>(999)), "999");
|
||||
}
|
||||
|
||||
TEST(enum_test, format_enum_with_hash_collision) {
|
||||
static_assert(fmt::detail::identifier_table_size<collision>() == 0);
|
||||
static_assert(fmt::detail::identifier_map_size<collision>() == 8);
|
||||
// Three of the four values want the same slot and the fourth takes the slot
|
||||
// next to it, filling the table to its maximum load factor of 0.5.
|
||||
static_assert(fmt::detail::identifier_slot(collision::c0) ==
|
||||
fmt::detail::identifier_slot(collision::c7));
|
||||
static_assert(fmt::detail::identifier_slot(collision::c0) ==
|
||||
fmt::detail::identifier_slot(collision::c15));
|
||||
static_assert(fmt::detail::identifier_slot(collision::c6) !=
|
||||
fmt::detail::identifier_slot(collision::c0));
|
||||
EXPECT_EQ(fmt::format("{}", collision::c0), "c0");
|
||||
EXPECT_EQ(fmt::format("{}", collision::c6), "c6");
|
||||
EXPECT_EQ(fmt::format("{}", collision::c7), "c7");
|
||||
EXPECT_EQ(fmt::format("{}", collision::c15), "c15");
|
||||
// A value that collides with the enumerators but doesn't match any of them
|
||||
// is rejected after probing the whole chain.
|
||||
static_assert(fmt::detail::identifier_slot(static_cast<collision>(23)) ==
|
||||
fmt::detail::identifier_slot(collision::c0));
|
||||
EXPECT_EQ(fmt::format("{}", static_cast<collision>(23)), "23");
|
||||
}
|
||||
|
||||
TEST(enum_test, format_enum_with_wrapping_hash_collision) {
|
||||
static_assert(fmt::detail::identifier_table_size<wrapping_collision>() == 0);
|
||||
static_assert(fmt::detail::identifier_map_size<wrapping_collision>() == 8);
|
||||
// All the values want the last slot, so the probe sequence wraps around.
|
||||
static_assert(fmt::detail::identifier_slot(wrapping_collision::w8) == 7);
|
||||
static_assert(fmt::detail::identifier_slot(wrapping_collision::w16) == 7);
|
||||
static_assert(fmt::detail::identifier_slot(wrapping_collision::w24) == 7);
|
||||
EXPECT_EQ(fmt::format("{}", wrapping_collision::w8), "w8");
|
||||
EXPECT_EQ(fmt::format("{}", wrapping_collision::w16), "w16");
|
||||
EXPECT_EQ(fmt::format("{}", wrapping_collision::w24), "w24");
|
||||
EXPECT_EQ(fmt::format("{}", static_cast<wrapping_collision>(33)), "33");
|
||||
}
|
||||
|
||||
TEST(enum_test, format_sparse_enum_alias) {
|
||||
// The first enumerator with a matching value is used in the hash table too.
|
||||
static_assert(fmt::detail::identifier_table_size<sparse_alias>() == 0);
|
||||
EXPECT_EQ(fmt::format("{}", sparse_alias::dup), "one");
|
||||
EXPECT_EQ(fmt::format("{}", sparse_alias::far), "far");
|
||||
}
|
||||
|
||||
TEST(enum_test, format_enum_with_extreme_values) {
|
||||
// The span of the values overflows the underlying type, so no table is used.
|
||||
static_assert(fmt::detail::identifier_table_size<extremes>() == 0);
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user