UDocumentation UE5.7 10.02.2026 (Source)
API documentation for Unreal Engine 5.7
GrowOnlyLockFreeHash.h
Go to the documentation of this file.
1// Copyright Epic Games, Inc. All Rights Reserved.
2
3#pragma once
4#include "CoreMinimal.h"
5#include "Misc/ScopeLock.h"
6
7// Hash table with fast lock free reads, that only supports insertion of items, and no modification of
8// values. KeyType must be an integer. EntryType should be a POD with an identifiable "empty" state
9// that can't occur in the table, and include the following member functions:
10//
11// KeyType GetKey() const; // Get the key from EntryType
12// ValueType GetValue() const; // Get the value from EntryType
13// bool IsEmpty() const; // Query whether EntryType is empty
14// void SetKeyValue(KeyType Key, ValueType Value); // Write key and value into EntryType (ATOMICALLY! See below)
15// static uint32 KeyHash(KeyType Key); // Convert Key to more well distributed hash
16// static void ClearEntries(EntryType* Entries, int32 EntryCount); // Fill an array of entries with empty values
17//
18// The function "SetKeyValue" must be multi-thread safe when writing new items! This means writing the
19// Key last and atomically, or writing the entire EntryType in a single write (say if the key and value
20// are packed into a single integer word). Inline is recommended, since these functions are called a
21// lot in the inner loop of the algorithm. A simple implementation of "KeyHash" can just return the
22// Key (if it's already reasonable as a hash), or mix the bits if better distribution is required. A
23// simple implementation of "ClearEntries" can just be a memset, if zero represents an empty entry.
24//
25// A set can be approximated by making "GetValue" a nop function, and just paying attention to the bool
26// result from FindEntry, although you do need to either reserve a certain Key as invalid, or add
27// space to store a valid flag as the Value. This class should only be used for small value types, as
28// the values are embedded into the hash table, and not stored separately.
29//
30// Writes are implemented using a lock -- it would be possible to make writes lock free (or lock free
31// when resizing doesn't occur), but it adds complexity. If we were to go that route, it would make
32// sense to create a fully generic lock free set, which would be much more involved to implement and
33// validate than this simple class, and might also offer somewhat worse read perf. Lock free containers
34// that support item removal either need additional synchronization overhead on readers, so writers can
35// tell if a reader is active and spin, or need graveyard markers and a garbage collection pass called
36// periodically, which makes it no longer a simple standalone container.
37//
38// Lock free reads are accomplished by the reader atomically pulling the hash table pointer from the
39// class. The hash table is self contained, with its size stored in the table itself, and hash tables
40// are not freed until the class's destruction. So if the table needs to be reallocated due to a write,
41// active readers will still have valid memory. This does mean that tables leak, but worst case, you
42// end up with half of the memory being waste. It would be possible to garbage collect the excess
43// tables, but you'd need some kind of global synchronization to make sure no readers are active.
44//
45// Besides cleanup of wasted tables, it might be useful to provide a function to clear a table. This
46// would involve clearing the Key for all the elements in the table (but leaving the memory allocated),
47// and can be done safely with active readers. It's not possible to safely remove individual items due
48// to the need to potentially move other items, which would break an active reader that has already
49// searched past a moved item. But in the case of removing all items, we don't care when a reader fails,
50// it's expected that eventually all readers will fail, regardless of where they are searching. A clear
51// function could be useful if a lot of the data you are caching is no longer used, and you want to
52// reset the cache.
53//
54template<typename EntryType, typename KeyType, typename ValueType>
56{
57public:
59 : Malloc(InMalloc), HashTable(nullptr)
60 {}
61
63 {
64 FHashHeader* HashTableNext;
65 for (FHashHeader* HashTableCurrent = HashTable; HashTableCurrent; HashTableCurrent = HashTableNext)
66 {
68
69 Malloc->Free(HashTableCurrent);
70 }
71 }
72
79 {
80 FScopeLock _(&WriteCriticalSection);
81 check(HashTable.load(std::memory_order_relaxed) == nullptr);
82
83 if (Count <= 0)
84 {
85 Count = DEFAULT_INITIAL_SIZE;
86 }
87 Count = FPlatformMath::RoundUpToPowerOfTwo(Count);
88 FHashHeader* HashTableLocal = (FHashHeader*)Malloc->Malloc(sizeof(FHashHeader) + (Count - 1) * sizeof(EntryType));
89
90 HashTableLocal->Next = nullptr;
91 HashTableLocal->TableSize = Count;
92 HashTableLocal->Used = 0;
93 EntryType::ClearEntries(HashTableLocal->Elements, Count);
94
95 HashTable.store(HashTableLocal, std::memory_order_release);
96 }
97
104 void Find(KeyType Key, ValueType *OutValue, bool* bIsAlreadyInTable = nullptr) const
105 {
106 FHashHeader* HashTableLocal = HashTable.load(std::memory_order_acquire);
107 if (HashTableLocal)
108 {
109 uint32 TableMask = HashTableLocal->TableSize - 1;
110
111 // Linear probing
112 for (uint32 TableIndex = EntryType::KeyHash(Key) & TableMask; !HashTableLocal->Elements[TableIndex].IsEmpty(); TableIndex = (TableIndex + 1) & TableMask)
113 {
114 if (HashTableLocal->Elements[TableIndex].GetKey() == Key)
115 {
116 if (OutValue)
117 {
118 *OutValue = HashTableLocal->Elements[TableIndex].GetValue();
119 }
121 {
122 *bIsAlreadyInTable = true;
123 }
124 return;
125 }
126 }
127 }
128
130 {
131 *bIsAlreadyInTable = false;
132 }
133 }
134
141 void Emplace(KeyType Key, ValueType Value, bool* bIsAlreadyInTable = nullptr)
142 {
143 FScopeLock _(&WriteCriticalSection);
144
145 // After locking, check if the item is already in the hash table.
146 ValueType ValueIgnore;
147 bool bFindResult;
149 if (bFindResult == true)
150 {
152 {
153 *bIsAlreadyInTable = true;
154 }
155 return;
156 }
157
158 // Check if there is space in the hash table for a new item. We resize when the hash
159 // table gets half full or more. @todo: allow client to specify max load factor?
160 FHashHeader* HashTableLocal = HashTable;
161
162 if (!HashTableLocal || (HashTableLocal->Used >= HashTableLocal->TableSize / 2))
163 {
164 int32 GrowCount = HashTableLocal ? HashTableLocal->TableSize * 2 : DEFAULT_INITIAL_SIZE;
165 FHashHeader* HashTableGrow = (FHashHeader*)Malloc->Malloc(sizeof(FHashHeader) + (GrowCount - 1) * sizeof(EntryType));
166
168 HashTableGrow->TableSize = GrowCount;
169 HashTableGrow->Used = 0;
170 EntryType::ClearEntries(HashTableGrow->Elements, GrowCount);
171
172 if (HashTableLocal)
173 {
174 // Copy existing elements from the old table to the new table
175 for (int32 TableIndex = 0; TableIndex < HashTableLocal->TableSize; TableIndex++)
176 {
177 EntryType& Entry = HashTableLocal->Elements[TableIndex];
178 if (!Entry.IsEmpty())
179 {
180 HashInsertInternal(HashTableGrow, Entry.GetKey(), Entry.GetValue());
181 }
182 }
183 }
184
186 HashTable.store(HashTableGrow, std::memory_order_release);
187 }
188
189 // Then add our new item
190 HashInsertInternal(HashTableLocal, Key, Value);
191
193 {
194 *bIsAlreadyInTable = false;
195 }
196 }
197
198 void FindOrAdd(KeyType Key, ValueType Value, bool* bIsAlreadyInTable = nullptr)
199 {
200 // Attempt to find the item lock free, before calling "Emplace", which locks the container
201 bool bFindResult;
202 ValueType IgnoreResult;
204 if (bFindResult)
205 {
207 {
208 *bIsAlreadyInTable = true;
209 }
210 return;
211 }
212
214 }
215
216private:
217 struct FHashHeader
218 {
219 FHashHeader* Next; // Old buffers are stored in a linked list for cleanup
220 int32 TableSize;
221 int32 Used;
222 EntryType Elements[1]; // Variable sized
223 };
224
225 FMalloc* Malloc;
226 std::atomic<FHashHeader*> HashTable;
227 FCriticalSection WriteCriticalSection;
228
229 static constexpr int32 DEFAULT_INITIAL_SIZE = 1024;
230
231 static void HashInsertInternal(FHashHeader* HashTableLocal, KeyType Key, ValueType Value)
232 {
233 int32 TableMask = HashTableLocal->TableSize - 1;
234
235 // Linear probing
236 for (int32 TableIndex = EntryType::KeyHash(Key) & TableMask;; TableIndex = (TableIndex + 1) & TableMask)
237 {
238 if (HashTableLocal->Elements[TableIndex].IsEmpty())
239 {
240 HashTableLocal->Elements[TableIndex].SetKeyValue(Key, Value);
241 HashTableLocal->Used++;
242 break;
243 }
244 }
245 }
246};
#define check(expr)
Definition AssertionMacros.h:314
FPlatformTypes::int32 int32
A 32-bit signed integer.
Definition Platform.h:1125
UE_FORCEINLINE_HINT TSharedRef< CastToType, Mode > StaticCastSharedRef(TSharedRef< CastFromType, Mode > const &InSharedRef)
Definition SharedPointer.h:127
UE::FPlatformRecursiveMutex FCriticalSection
Definition CriticalSection.h:53
uint32_t uint32
Definition binka_ue_file_header.h:6
Definition MemoryBase.h:99
virtual void Free(void *Original)=0
virtual void * Malloc(SIZE_T Count, uint32 Alignment=DEFAULT_ALIGNMENT)=0
Definition ScopeLock.h:141
Definition GrowOnlyLockFreeHash.h:56
void Reserve(int32 Count)
Definition GrowOnlyLockFreeHash.h:78
TGrowOnlyLockFreeHash(FMalloc *InMalloc)
Definition GrowOnlyLockFreeHash.h:58
void Find(KeyType Key, ValueType *OutValue, bool *bIsAlreadyInTable=nullptr) const
Definition GrowOnlyLockFreeHash.h:104
void FindOrAdd(KeyType Key, ValueType Value, bool *bIsAlreadyInTable=nullptr)
Definition GrowOnlyLockFreeHash.h:198
~TGrowOnlyLockFreeHash()
Definition GrowOnlyLockFreeHash.h:62
void Emplace(KeyType Key, ValueType Value, bool *bIsAlreadyInTable=nullptr)
Definition GrowOnlyLockFreeHash.h:141