Skip to content

Commit 59a28fe

Browse files
awesomeklinggmta
authored andcommitted
AK: Store hash with HashTable entry to avoid expensive equality checks
When T in HashTable<T> 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<String, V> where collisions previously meant we may have to churn through multiple O(n) equality checks.
1 parent c077ba9 commit 59a28fe

File tree

13 files changed

+57
-8
lines changed

13 files changed

+57
-8
lines changed

AK/FlyString.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -200,6 +200,7 @@ class Optional<FlyString> : public OptionalBase<FlyString> {
200200
template<>
201201
struct Traits<FlyString> : public DefaultTraits<FlyString> {
202202
static unsigned hash(FlyString const&);
203+
static constexpr bool may_have_slow_equality_check() { return false; }
203204
};
204205

205206
template<>

AK/HashMap.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ class HashMap {
2626
};
2727

2828
struct EntryTraits {
29+
static constexpr bool may_have_slow_equality_check() { return KeyTraits::may_have_slow_equality_check(); }
2930
static unsigned hash(Entry const& entry) { return KeyTraits::hash(entry.key); }
3031
static bool equals(Entry const& a, Entry const& b) { return KeyTraits::equals(a.key, b.key); }
3132
};

AK/HashTable.h

Lines changed: 34 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright (c) 2018-2020, Andreas Kling <andreas@ladybird.org>
2+
* Copyright (c) 2018-2025, Andreas Kling <andreas@ladybird.org>
33
* Copyright (c) 2023, Jelle Raaijmakers <jelle@ladybird.org>
44
*
55
* SPDX-License-Identifier: BSD-2-Clause
@@ -123,8 +123,29 @@ class HashTable {
123123
static constexpr size_t grow_at_load_factor_percent = 80;
124124
static constexpr size_t grow_capacity_increase_percent = 60;
125125

126+
struct StoredHash {
127+
void set([[maybe_unused]] u32 h)
128+
{
129+
if constexpr (TraitsForT::may_have_slow_equality_check()) {
130+
hash = h;
131+
}
132+
}
133+
bool check(u32 h)
134+
{
135+
if constexpr (TraitsForT::may_have_slow_equality_check()) {
136+
// If equality checks may be slow, we always store the hash and compare it first.
137+
return hash == h;
138+
} else {
139+
// If equality checks are fast, we don't store the hash and always return true.
140+
return true;
141+
}
142+
}
143+
u32 hash;
144+
};
145+
126146
struct Bucket {
127147
BucketState state;
148+
StoredHash hash;
128149
alignas(T) u8 storage[sizeof(T)];
129150
T* slot() { return reinterpret_cast<T*>(storage); }
130151
T const* slot() const { return reinterpret_cast<T const*>(storage); }
@@ -134,6 +155,7 @@ class HashTable {
134155
OrderedBucket* previous;
135156
OrderedBucket* next;
136157
BucketState state;
158+
StoredHash hash;
137159
alignas(T) u8 storage[sizeof(T)];
138160
T* slot() { return reinterpret_cast<T*>(storage); }
139161
T const* slot() const { return reinterpret_cast<T const*>(storage); }
@@ -583,15 +605,15 @@ class HashTable {
583605
if (is_empty())
584606
return nullptr;
585607

586-
hash %= m_capacity;
608+
size_t bucket_index = hash % m_capacity;
587609
for (;;) {
588-
auto* bucket = &m_buckets[hash];
610+
auto* bucket = &m_buckets[bucket_index];
589611
if (bucket->state == BucketState::Free)
590612
return nullptr;
591-
if (predicate(*bucket->slot()))
613+
if (bucket->hash.check(hash) && predicate(*bucket->slot()))
592614
return bucket;
593-
if (++hash == m_capacity) [[unlikely]]
594-
hash = 0;
615+
if (++bucket_index == m_capacity) [[unlikely]]
616+
bucket_index = 0;
595617
}
596618
}
597619

@@ -663,7 +685,8 @@ class HashTable {
663685
}
664686
};
665687

666-
auto bucket_index = TraitsForT::hash(value) % m_capacity;
688+
u32 const hash = TraitsForT::hash(value);
689+
auto bucket_index = hash % m_capacity;
667690
size_t probe_length = 0;
668691
for (;;) {
669692
auto* bucket = &m_buckets[bucket_index];
@@ -672,13 +695,15 @@ class HashTable {
672695
if (bucket->state == BucketState::Free) {
673696
new (bucket->slot()) T(forward<U>(value));
674697
bucket->state = bucket_state_for_probe_length(probe_length);
698+
bucket->hash.set(hash);
675699
update_collection_for_new_bucket(*bucket);
676700
++m_size;
677701
return HashSetResult::InsertedNewEntry;
678702
}
679703

680704
// The bucket is already used, does it have an identical value?
681-
if (TraitsForT::equals(*bucket->slot(), static_cast<T const&>(value))) {
705+
if (bucket->hash.check(hash)
706+
&& TraitsForT::equals(*bucket->slot(), static_cast<T const&>(value))) {
682707
if (existing_entry_behavior == HashSetExistingEntryBehavior::Replace) {
683708
(*bucket->slot()) = forward<U>(value);
684709
return HashSetResult::ReplacedExistingEntry;
@@ -697,6 +722,7 @@ class HashTable {
697722
// Write new bucket
698723
new (bucket->slot()) T(forward<U>(value));
699724
bucket->state = bucket_state_for_probe_length(probe_length);
725+
bucket->hash.set(hash);
700726
probe_length = target_probe_length;
701727
if constexpr (IsOrdered)
702728
bucket->next = nullptr;

AK/NonnullOwnPtr.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,7 @@ struct Traits<NonnullOwnPtr<T>> : public DefaultTraits<NonnullOwnPtr<T>> {
169169
using ConstPeekType = T const*;
170170
static unsigned hash(NonnullOwnPtr<T> const& p) { return ptr_hash(p.ptr()); }
171171
static bool equals(NonnullOwnPtr<T> const& a, NonnullOwnPtr<T> const& b) { return a.ptr() == b.ptr(); }
172+
static constexpr bool may_have_slow_equality_check() { return false; }
172173
};
173174

174175
template<typename T, typename U>

AK/NonnullRawPtr.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ requires(!IsLvalueReference<T> && !IsRvalueReference<T>) class [[nodiscard]] Non
5050
template<typename T>
5151
struct Traits<NonnullRawPtr<T>> : public DefaultTraits<NonnullRawPtr<T>> {
5252
static unsigned hash(NonnullRawPtr<T> const& handle) { return Traits<T>::hash(handle); }
53+
static constexpr bool may_have_slow_equality_check() { return false; }
5354
};
5455

5556
namespace Detail {

AK/NonnullRefPtr.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -276,6 +276,7 @@ struct Traits<NonnullRefPtr<T>> : public DefaultTraits<NonnullRefPtr<T>> {
276276
using ConstPeekType = T const*;
277277
static unsigned hash(NonnullRefPtr<T> const& p) { return ptr_hash(p.ptr()); }
278278
static bool equals(NonnullRefPtr<T> const& a, NonnullRefPtr<T> const& b) { return a.ptr() == b.ptr(); }
279+
static constexpr bool may_have_slow_equality_check() { return false; }
279280
};
280281

281282
}

AK/OwnPtr.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -198,6 +198,7 @@ struct Traits<OwnPtr<T>> : public DefaultTraits<OwnPtr<T>> {
198198
using ConstPeekType = T const*;
199199
static unsigned hash(OwnPtr<T> const& p) { return ptr_hash(p.ptr()); }
200200
static bool equals(OwnPtr<T> const& a, OwnPtr<T> const& b) { return a.ptr() == b.ptr(); }
201+
static constexpr bool may_have_slow_equality_check() { return false; }
201202
};
202203

203204
template<typename T>

AK/RefPtr.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -298,6 +298,7 @@ struct Traits<RefPtr<T>> : public DefaultTraits<RefPtr<T>> {
298298
using ConstPeekType = T const*;
299299
static unsigned hash(RefPtr<T> const& p) { return ptr_hash(p.ptr()); }
300300
static bool equals(RefPtr<T> const& a, RefPtr<T> const& b) { return a.ptr() == b.ptr(); }
301+
static constexpr bool may_have_slow_equality_check() { return false; }
301302
};
302303

303304
template<typename T, typename U>

AK/Traits.h

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,9 @@ struct DefaultTraits {
2323
static constexpr bool equals(T const& a, T const& b) { return a == b; }
2424
template<Concepts::HashCompatible<T> U>
2525
static bool equals(T const& self, U const& other) { return self == other; }
26+
// NOTE: Override this to say false if your type has a fast equality check.
27+
// If equality checks are fast, we won't store hashes in HashTable/HashMap,
28+
static constexpr bool may_have_slow_equality_check() { return true; }
2629
};
2730

2831
template<typename T>
@@ -38,6 +41,8 @@ template<Integral T>
3841
struct Traits<T> : public DefaultTraits<T> {
3942
static constexpr bool is_trivial() { return true; }
4043
static constexpr bool is_trivially_serializable() { return true; }
44+
// NOTE: Trivial types always have fast equality checks.
45+
static constexpr bool may_have_slow_equality_check() { return false; }
4146
static unsigned hash(T value)
4247
{
4348
if constexpr (sizeof(T) < 8)
@@ -51,6 +56,7 @@ template<FloatingPoint T>
5156
struct Traits<T> : public DefaultTraits<T> {
5257
static constexpr bool is_trivial() { return true; }
5358
static constexpr bool is_trivially_serializable() { return true; }
59+
static constexpr bool may_have_slow_equality_check() { return false; }
5460
static unsigned hash(T value)
5561
{
5662
if constexpr (sizeof(T) < 8)
@@ -64,12 +70,16 @@ template<typename T>
6470
requires(IsPointer<T> && !Detail::IsPointerOfType<char, T>) struct Traits<T> : public DefaultTraits<T> {
6571
static unsigned hash(T p) { return ptr_hash(bit_cast<FlatPtr>(p)); }
6672
static constexpr bool is_trivial() { return true; }
73+
// NOTE: Trivial types always have fast equality checks.
74+
static constexpr bool may_have_slow_equality_check() { return false; }
6775
};
6876

6977
template<Enum T>
7078
struct Traits<T> : public DefaultTraits<T> {
7179
static unsigned hash(T value) { return Traits<UnderlyingType<T>>::hash(to_underlying(value)); }
7280
static constexpr bool is_trivial() { return Traits<UnderlyingType<T>>::is_trivial(); }
81+
// NOTE: Trivial types always have fast equality checks.
82+
static constexpr bool may_have_slow_equality_check() { return !is_trivial(); }
7383
static constexpr bool is_trivially_serializable() { return Traits<UnderlyingType<T>>::is_trivially_serializable(); }
7484
};
7585

@@ -78,6 +88,8 @@ requires(Detail::IsPointerOfType<char, T>) struct Traits<T> : public DefaultTrai
7888
static unsigned hash(T const value) { return string_hash(value, strlen(value)); }
7989
static constexpr bool equals(T const a, T const b) { return strcmp(a, b); }
8090
static constexpr bool is_trivial() { return true; }
91+
// NOTE: Trivial types always have fast equality checks.
92+
static constexpr bool may_have_slow_equality_check() { return false; }
8193
};
8294

8395
}

AK/Utf16FlyString.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -251,6 +251,7 @@ class Optional<Utf16FlyString> : public OptionalBase<Utf16FlyString> {
251251
template<>
252252
struct Traits<Utf16FlyString> : public DefaultTraits<Utf16FlyString> {
253253
static unsigned hash(Utf16FlyString const& string) { return string.hash(); }
254+
static constexpr bool may_have_slow_equality_check() { return false; }
254255
};
255256

256257
template<>

0 commit comments

Comments
 (0)