From 59a28febc97e450befd5bdc21c93c245e2aa7b91 Mon Sep 17 00:00:00 2001 From: Andreas Kling Date: Thu, 18 Sep 2025 18:28:54 +0200 Subject: [PATCH] AK: Store hash with HashTable entry to avoid expensive equality checks When T in HashTable has a potentially slow equality check, it can be very profitable to check for a matching hash before full equality. This patch adds may_have_slow_equality_check() to AK::Traits and defaults it to true. For trivial types (pointers, integers, etc) we default it to false. This means we skip the hash check when the equality check would be a single-CPU-word compare anyway. This synergizes really well with things like HashMap where collisions previously meant we may have to churn through multiple O(n) equality checks. --- AK/FlyString.h | 1 + AK/HashMap.h | 1 + AK/HashTable.h | 42 +++++++++++++++---- AK/NonnullOwnPtr.h | 1 + AK/NonnullRawPtr.h | 1 + AK/NonnullRefPtr.h | 1 + AK/OwnPtr.h | 1 + AK/RefPtr.h | 1 + AK/Traits.h | 12 ++++++ AK/Utf16FlyString.h | 1 + AK/WeakPtr.h | 1 + Libraries/LibGC/Ptr.h | 1 + .../GenerateNamedCharacterReferences.cpp | 1 + 13 files changed, 57 insertions(+), 8 deletions(-) diff --git a/AK/FlyString.h b/AK/FlyString.h index b941e129fc..be5f0f7cd0 100644 --- a/AK/FlyString.h +++ b/AK/FlyString.h @@ -200,6 +200,7 @@ private: template<> struct Traits : public DefaultTraits { static unsigned hash(FlyString const&); + static constexpr bool may_have_slow_equality_check() { return false; } }; template<> diff --git a/AK/HashMap.h b/AK/HashMap.h index 6787cc456d..731aa34680 100644 --- a/AK/HashMap.h +++ b/AK/HashMap.h @@ -26,6 +26,7 @@ private: }; struct EntryTraits { + static constexpr bool may_have_slow_equality_check() { return KeyTraits::may_have_slow_equality_check(); } static unsigned hash(Entry const& entry) { return KeyTraits::hash(entry.key); } static bool equals(Entry const& a, Entry const& b) { return KeyTraits::equals(a.key, b.key); } }; diff --git a/AK/HashTable.h b/AK/HashTable.h index 5a846e09ba..47e9f6c733 100644 --- a/AK/HashTable.h +++ b/AK/HashTable.h @@ -1,5 +1,5 @@ /* - * Copyright (c) 2018-2020, Andreas Kling + * Copyright (c) 2018-2025, Andreas Kling * Copyright (c) 2023, Jelle Raaijmakers * * SPDX-License-Identifier: BSD-2-Clause @@ -123,8 +123,29 @@ class HashTable { static constexpr size_t grow_at_load_factor_percent = 80; static constexpr size_t grow_capacity_increase_percent = 60; + struct StoredHash { + void set([[maybe_unused]] u32 h) + { + if constexpr (TraitsForT::may_have_slow_equality_check()) { + hash = h; + } + } + bool check(u32 h) + { + if constexpr (TraitsForT::may_have_slow_equality_check()) { + // If equality checks may be slow, we always store the hash and compare it first. + return hash == h; + } else { + // If equality checks are fast, we don't store the hash and always return true. + return true; + } + } + u32 hash; + }; + struct Bucket { BucketState state; + StoredHash hash; alignas(T) u8 storage[sizeof(T)]; T* slot() { return reinterpret_cast(storage); } T const* slot() const { return reinterpret_cast(storage); } @@ -134,6 +155,7 @@ class HashTable { OrderedBucket* previous; OrderedBucket* next; BucketState state; + StoredHash hash; alignas(T) u8 storage[sizeof(T)]; T* slot() { return reinterpret_cast(storage); } T const* slot() const { return reinterpret_cast(storage); } @@ -583,15 +605,15 @@ private: if (is_empty()) return nullptr; - hash %= m_capacity; + size_t bucket_index = hash % m_capacity; for (;;) { - auto* bucket = &m_buckets[hash]; + auto* bucket = &m_buckets[bucket_index]; if (bucket->state == BucketState::Free) return nullptr; - if (predicate(*bucket->slot())) + if (bucket->hash.check(hash) && predicate(*bucket->slot())) return bucket; - if (++hash == m_capacity) [[unlikely]] - hash = 0; + if (++bucket_index == m_capacity) [[unlikely]] + bucket_index = 0; } } @@ -663,7 +685,8 @@ private: } }; - auto bucket_index = TraitsForT::hash(value) % m_capacity; + u32 const hash = TraitsForT::hash(value); + auto bucket_index = hash % m_capacity; size_t probe_length = 0; for (;;) { auto* bucket = &m_buckets[bucket_index]; @@ -672,13 +695,15 @@ private: if (bucket->state == BucketState::Free) { new (bucket->slot()) T(forward(value)); bucket->state = bucket_state_for_probe_length(probe_length); + bucket->hash.set(hash); update_collection_for_new_bucket(*bucket); ++m_size; return HashSetResult::InsertedNewEntry; } // The bucket is already used, does it have an identical value? - if (TraitsForT::equals(*bucket->slot(), static_cast(value))) { + if (bucket->hash.check(hash) + && TraitsForT::equals(*bucket->slot(), static_cast(value))) { if (existing_entry_behavior == HashSetExistingEntryBehavior::Replace) { (*bucket->slot()) = forward(value); return HashSetResult::ReplacedExistingEntry; @@ -697,6 +722,7 @@ private: // Write new bucket new (bucket->slot()) T(forward(value)); bucket->state = bucket_state_for_probe_length(probe_length); + bucket->hash.set(hash); probe_length = target_probe_length; if constexpr (IsOrdered) bucket->next = nullptr; diff --git a/AK/NonnullOwnPtr.h b/AK/NonnullOwnPtr.h index a87ce31955..10958af04f 100644 --- a/AK/NonnullOwnPtr.h +++ b/AK/NonnullOwnPtr.h @@ -169,6 +169,7 @@ struct Traits> : public DefaultTraits> { using ConstPeekType = T const*; static unsigned hash(NonnullOwnPtr const& p) { return ptr_hash(p.ptr()); } static bool equals(NonnullOwnPtr const& a, NonnullOwnPtr const& b) { return a.ptr() == b.ptr(); } + static constexpr bool may_have_slow_equality_check() { return false; } }; template diff --git a/AK/NonnullRawPtr.h b/AK/NonnullRawPtr.h index b290d60f27..3e8b17846a 100644 --- a/AK/NonnullRawPtr.h +++ b/AK/NonnullRawPtr.h @@ -50,6 +50,7 @@ private: template struct Traits> : public DefaultTraits> { static unsigned hash(NonnullRawPtr const& handle) { return Traits::hash(handle); } + static constexpr bool may_have_slow_equality_check() { return false; } }; namespace Detail { diff --git a/AK/NonnullRefPtr.h b/AK/NonnullRefPtr.h index 949d14bdb1..a441bfc899 100644 --- a/AK/NonnullRefPtr.h +++ b/AK/NonnullRefPtr.h @@ -276,6 +276,7 @@ struct Traits> : public DefaultTraits> { using ConstPeekType = T const*; static unsigned hash(NonnullRefPtr const& p) { return ptr_hash(p.ptr()); } static bool equals(NonnullRefPtr const& a, NonnullRefPtr const& b) { return a.ptr() == b.ptr(); } + static constexpr bool may_have_slow_equality_check() { return false; } }; } diff --git a/AK/OwnPtr.h b/AK/OwnPtr.h index 605009b811..da72e5c2b7 100644 --- a/AK/OwnPtr.h +++ b/AK/OwnPtr.h @@ -198,6 +198,7 @@ struct Traits> : public DefaultTraits> { using ConstPeekType = T const*; static unsigned hash(OwnPtr const& p) { return ptr_hash(p.ptr()); } static bool equals(OwnPtr const& a, OwnPtr const& b) { return a.ptr() == b.ptr(); } + static constexpr bool may_have_slow_equality_check() { return false; } }; template diff --git a/AK/RefPtr.h b/AK/RefPtr.h index d02ace7e4f..9083b4708b 100644 --- a/AK/RefPtr.h +++ b/AK/RefPtr.h @@ -298,6 +298,7 @@ struct Traits> : public DefaultTraits> { using ConstPeekType = T const*; static unsigned hash(RefPtr const& p) { return ptr_hash(p.ptr()); } static bool equals(RefPtr const& a, RefPtr const& b) { return a.ptr() == b.ptr(); } + static constexpr bool may_have_slow_equality_check() { return false; } }; template diff --git a/AK/Traits.h b/AK/Traits.h index f5b17e2ec1..31053f6bcd 100644 --- a/AK/Traits.h +++ b/AK/Traits.h @@ -23,6 +23,9 @@ struct DefaultTraits { static constexpr bool equals(T const& a, T const& b) { return a == b; } template U> static bool equals(T const& self, U const& other) { return self == other; } + // NOTE: Override this to say false if your type has a fast equality check. + // If equality checks are fast, we won't store hashes in HashTable/HashMap, + static constexpr bool may_have_slow_equality_check() { return true; } }; template @@ -38,6 +41,8 @@ template struct Traits : public DefaultTraits { static constexpr bool is_trivial() { return true; } static constexpr bool is_trivially_serializable() { return true; } + // NOTE: Trivial types always have fast equality checks. + static constexpr bool may_have_slow_equality_check() { return false; } static unsigned hash(T value) { if constexpr (sizeof(T) < 8) @@ -51,6 +56,7 @@ template struct Traits : public DefaultTraits { static constexpr bool is_trivial() { return true; } static constexpr bool is_trivially_serializable() { return true; } + static constexpr bool may_have_slow_equality_check() { return false; } static unsigned hash(T value) { if constexpr (sizeof(T) < 8) @@ -64,12 +70,16 @@ template requires(IsPointer && !Detail::IsPointerOfType) struct Traits : public DefaultTraits { static unsigned hash(T p) { return ptr_hash(bit_cast(p)); } static constexpr bool is_trivial() { return true; } + // NOTE: Trivial types always have fast equality checks. + static constexpr bool may_have_slow_equality_check() { return false; } }; template struct Traits : public DefaultTraits { static unsigned hash(T value) { return Traits>::hash(to_underlying(value)); } static constexpr bool is_trivial() { return Traits>::is_trivial(); } + // NOTE: Trivial types always have fast equality checks. + static constexpr bool may_have_slow_equality_check() { return !is_trivial(); } static constexpr bool is_trivially_serializable() { return Traits>::is_trivially_serializable(); } }; @@ -78,6 +88,8 @@ requires(Detail::IsPointerOfType) struct Traits : public DefaultTrai static unsigned hash(T const value) { return string_hash(value, strlen(value)); } static constexpr bool equals(T const a, T const b) { return strcmp(a, b); } static constexpr bool is_trivial() { return true; } + // NOTE: Trivial types always have fast equality checks. + static constexpr bool may_have_slow_equality_check() { return false; } }; } diff --git a/AK/Utf16FlyString.h b/AK/Utf16FlyString.h index 44b702c2ab..513087e463 100644 --- a/AK/Utf16FlyString.h +++ b/AK/Utf16FlyString.h @@ -251,6 +251,7 @@ private: template<> struct Traits : public DefaultTraits { static unsigned hash(Utf16FlyString const& string) { return string.hash(); } + static constexpr bool may_have_slow_equality_check() { return false; } }; template<> diff --git a/AK/WeakPtr.h b/AK/WeakPtr.h index b3814bec00..f24c411ad5 100644 --- a/AK/WeakPtr.h +++ b/AK/WeakPtr.h @@ -212,6 +212,7 @@ struct Traits> : public DefaultTraits> { using ConstPeekType = T const*; static unsigned hash(WeakPtr const& p) { return ptr_hash(p.ptr()); } static bool equals(WeakPtr const& a, WeakPtr const& b) { return a.ptr() == b.ptr(); } + static constexpr bool may_have_slow_equality_check() { return false; } }; } diff --git a/Libraries/LibGC/Ptr.h b/Libraries/LibGC/Ptr.h index 7a809eb402..1276705da6 100644 --- a/Libraries/LibGC/Ptr.h +++ b/Libraries/LibGC/Ptr.h @@ -235,6 +235,7 @@ struct Traits> : public DefaultTraits> { { return Traits::hash(value.ptr()); } + static constexpr bool may_have_slow_equality_check() { return false; } }; template diff --git a/Meta/Lagom/Tools/CodeGenerators/LibWeb/GenerateNamedCharacterReferences.cpp b/Meta/Lagom/Tools/CodeGenerators/LibWeb/GenerateNamedCharacterReferences.cpp index db45f2a45f..d6b882ee15 100644 --- a/Meta/Lagom/Tools/CodeGenerators/LibWeb/GenerateNamedCharacterReferences.cpp +++ b/Meta/Lagom/Tools/CodeGenerators/LibWeb/GenerateNamedCharacterReferences.cpp @@ -269,6 +269,7 @@ static u8 ascii_alphabetic_to_index(u8 c) class Node final : public RefCounted { private: struct NonnullRefPtrNodeTraits { + static constexpr bool may_have_slow_equality_check() { return true; } static unsigned hash(NonnullRefPtr const& node) { u32 hash = 0;