//========= Copyright Valve Corporation, All rights reserved. ============// // // Purpose: a fast growable hashtable with stored hashes, L2-friendly behavior. // Useful as a string dictionary or a low-overhead set/map for small POD types. // // Usage notes: // - handles are NOT STABLE across element removal! use RemoveAndAdvance() // if you are removing elements while iterating through the hashtable. // Use CUtlStableHashtable if you need stable handles (less efficient). // - Insert() first searches for an existing match and returns it if found // - a value type of "empty_t" can be used to eliminate value storage and // switch Element() to return const Key references instead of values // - an extra user flag bit is accessible via Get/SetUserFlag() // - hash function pointer / functor is exposed via GetHashRef() // - comparison function pointer / functor is exposed via GetEqualRef() // - if your value type cannot be copy-constructed, use key-only Insert() // to default-initialize the value and then manipulate it afterwards. // // Implementation notes: // - overall hash table load is kept between .25 and .75 // - items which would map to the same ideal slot are chained together // - chained items are stored sequentially in adjacent free spaces // - "root" entries are prioritized over chained entries; if a // slot is not occupied by an item in its root position, the table // is guaranteed to contain no keys which would hash to that slot. // - new items go at the head of the chain (ie, in their root slot) // and evict / "bump" any chained entries which occupy that slot // - chain-following skips over unused holes and continues examining // table entries until a chain entry with FLAG_LAST is encountered // // CUtlHashtable< uint32 > setOfIntegers; // CUtlHashtable< const char* > setOfStringPointers; // CUtlHashtable< int, CUtlVector > mapFromIntsToArrays; // // $NoKeywords: $ // // A closed-form (open addressing) hashtable with linear sequential probing. //=============================================================================// #ifndef UTLHASHTABLE_H #define UTLHASHTABLE_H #pragma once #include "mathlib/mathlib.h" #include "utlcommon.h" #include "utllinkedlist.h" #include "utlmemory.h" //----------------------------------------------------------------------------- // Henry Goffin (henryg) was here. Questions? Bugs? Go slap him around a bit. //----------------------------------------------------------------------------- typedef unsigned int UtlHashHandle_t; #define FOR_EACH_HASHTABLE(table, iter) \ for (UtlHashHandle_t iter = (table).FirstHandle(); \ iter != (table).InvalidHandle(); iter = (table).NextHandle(iter)) // CUtlHashtableEntry selects between 16 and 32 bit storage backing // for flags_and_hash depending on the size of the stored types. template class CUtlHashtableEntry { public: typedef CUtlKeyValuePair KVPair; enum { INT16_STORAGE = (sizeof(KVPair) <= 2) }; typedef typename CTypeSelect::type storage_t; enum { FLAG_FREE = INT16_STORAGE ? 0x8000 : 0x80000000, // must be high bit for IsValid // and IdealIndex to work FLAG_LAST = INT16_STORAGE ? 0x4000 : 0x40000000, MASK_HASH = INT16_STORAGE ? 0x3FFF : 0x3FFFFFFF }; storage_t flags_and_hash; storage_t data[(sizeof(KVPair) + sizeof(storage_t) - 1) / sizeof(storage_t)]; bool IsValid() const { return flags_and_hash >= 0; } void MarkInvalid() { int32 flag = FLAG_FREE; flags_and_hash = (storage_t)flag; } const KVPair *Raw() const { return reinterpret_cast(&data[0]); } const KVPair *operator->() const { Assert(IsValid()); return reinterpret_cast(&data[0]); } KVPair *Raw() { return reinterpret_cast(&data[0]); } KVPair *operator->() { Assert(IsValid()); return reinterpret_cast(&data[0]); } // Returns the ideal index of the data in this slot, or all bits set if // invalid uint32 FORCEINLINE IdealIndex(uint32 slotmask) const { return IdealIndex(flags_and_hash, slotmask) | ((int32)flags_and_hash >> 31); } // Use template tricks to fully define only one function that takes either // 16 or 32 bits and performs different logic without using "if ( // INT16_STORAGE )", because GCC and MSVC sometimes have trouble removing // the constant branch, which is dumb... but whatever. 16-bit hashes are // simply too narrow for large hashtables; more mask bits than hash bits! So // we duplicate the hash bits. (Note: h *= MASK_HASH+2 is the same as h += // h<::type uint32_if16BitStorage; typedef typename CTypeSelect::type uint32_if32BitStorage; static FORCEINLINE uint32 IdealIndex(uint32_if16BitStorage h, uint32 m) { h &= MASK_HASH; h *= MASK_HASH + 2; return h & m; } static FORCEINLINE uint32 IdealIndex(uint32_if32BitStorage h, uint32 m) { return h & m; } // More efficient than memcpy for the small types that are stored in a // hashtable void MoveDataFrom(CUtlHashtableEntry &src) { storage_t *RESTRICT srcData = &src.data[0]; for (int i = 0; i < ARRAYSIZE(data); ++i) { data[i] = srcData[i]; } } }; template , typename KeyIsEqualT = DefaultEqualFunctor, typename AlternateKeyT = typename ArgumentTypeInfo::Alt_t> class CUtlHashtable { public: typedef UtlHashHandle_t handle_t; protected: typedef CUtlKeyValuePair KVPair; typedef typename ArgumentTypeInfo::Arg_t KeyArg_t; typedef typename ArgumentTypeInfo::Arg_t ValueArg_t; typedef typename ArgumentTypeInfo::Arg_t KeyAlt_t; typedef CUtlHashtableEntry entry_t; enum { FLAG_FREE = entry_t::FLAG_FREE }; enum { FLAG_LAST = entry_t::FLAG_LAST }; enum { MASK_HASH = entry_t::MASK_HASH }; CUtlMemory m_table; int m_nUsed; int m_nMinSize; bool m_bSizeLocked; KeyIsEqualT m_eq; KeyHashT m_hash; // Allocate an empty table and then re-insert all existing entries. void DoRealloc(int size); // Move an existing entry to a free slot, leaving a hole behind void BumpEntry(unsigned int idx); // Insert an unconstructed KVPair at the primary slot int DoInsertUnconstructed(unsigned int h, bool allowGrow); // Implementation for Insert functions, constructs a KVPair // with either a default-construted or copy-constructed value template handle_t DoInsert(KeyParamT k, unsigned int h); template handle_t DoInsert(KeyParamT k, typename ArgumentTypeInfo::Arg_t v, unsigned int h, bool *pDidInsert); template handle_t DoInsertNoCheck(KeyParamT k, typename ArgumentTypeInfo::Arg_t v, unsigned int h); // Key lookup. Can also return previous-in-chain if result is chained. template handle_t DoLookup(KeyParamT x, unsigned int h, handle_t *pPreviousInChain) const; // Remove single element by key + hash. Returns the index of the new hole // that was created. Returns InvalidHandle() if element was not found. template int DoRemove(KeyParamT x, unsigned int h); // Friend CUtlStableHashtable so that it can call our Do* functions directly template friend class CUtlStableHashtable; public: explicit CUtlHashtable(int minimumSize = 32) : m_nUsed(0), m_nMinSize(MAX(8, minimumSize)), m_bSizeLocked(false), m_eq(), m_hash() {} CUtlHashtable(int minimumSize, const KeyHashT &hash, KeyIsEqualT const &eq = KeyIsEqualT()) : m_nUsed(0), m_nMinSize(MAX(8, minimumSize)), m_bSizeLocked(false), m_eq(eq), m_hash(hash) {} CUtlHashtable(entry_t *pMemory, unsigned int nCount, const KeyHashT &hash = KeyHashT(), KeyIsEqualT const &eq = KeyIsEqualT()) : m_nUsed(0), m_nMinSize(8), m_bSizeLocked(false), m_eq(eq), m_hash(hash) { SetExternalBuffer(pMemory, nCount); } ~CUtlHashtable() { RemoveAll(); } CUtlHashtable &operator=(CUtlHashtable const &src); // Set external memory void SetExternalBuffer(byte *pRawBuffer, unsigned int nBytes, bool bAssumeOwnership = false, bool bGrowable = false); void SetExternalBuffer(entry_t *pBuffer, unsigned int nSize, bool bAssumeOwnership = false, bool bGrowable = false); // Functor/function-pointer access KeyHashT &GetHashRef() { return m_hash; } KeyIsEqualT &GetEqualRef() { return m_eq; } KeyHashT const &GetHashRef() const { return m_hash; } KeyIsEqualT const &GetEqualRef() const { return m_eq; } // Handle validation bool IsValidHandle(handle_t idx) const { return (unsigned)idx < (unsigned)m_table.Count() && m_table[idx].IsValid(); } static handle_t InvalidHandle() { return (handle_t)-1; } // Iteration functions handle_t FirstHandle() const { return NextHandle((handle_t)-1); } handle_t NextHandle(handle_t start) const; // Returns the number of unique keys in the table int Count() const { return m_nUsed; } // Key lookup, returns InvalidHandle() if not found handle_t Find(KeyArg_t k) const { return DoLookup(k, m_hash(k), NULL); } handle_t Find(KeyArg_t k, unsigned int hash) const { Assert(hash == m_hash(k)); return DoLookup(k, hash, NULL); } // Alternate-type key lookup, returns InvalidHandle() if not found handle_t Find(KeyAlt_t k) const { return DoLookup(k, m_hash(k), NULL); } handle_t Find(KeyAlt_t k, unsigned int hash) const { Assert(hash == m_hash(k)); return DoLookup(k, hash, NULL); } // True if the key is in the table bool HasElement(KeyArg_t k) const { return InvalidHandle() != Find(k); } bool HasElement(KeyAlt_t k) const { return InvalidHandle() != Find(k); } // Key insertion or lookup, always returns a valid handle handle_t Insert(KeyArg_t k) { return DoInsert(k, m_hash(k)); } handle_t Insert(KeyArg_t k, ValueArg_t v, bool *pDidInsert = NULL) { return DoInsert(k, v, m_hash(k), pDidInsert); } handle_t Insert(KeyArg_t k, ValueArg_t v, unsigned int hash, bool *pDidInsert = NULL) { Assert(hash == m_hash(k)); return DoInsert(k, v, hash, pDidInsert); } // Alternate-type key insertion or lookup, always returns a valid handle handle_t Insert(KeyAlt_t k) { return DoInsert(k, m_hash(k)); } handle_t Insert(KeyAlt_t k, ValueArg_t v, bool *pDidInsert = NULL) { return DoInsert(k, v, m_hash(k), pDidInsert); } handle_t Insert(KeyAlt_t k, ValueArg_t v, unsigned int hash, bool *pDidInsert = NULL) { Assert(hash == m_hash(k)); return DoInsert(k, v, hash, pDidInsert); } // Key removal, returns false if not found bool Remove(KeyArg_t k) { return DoRemove(k, m_hash(k)) >= 0; } bool Remove(KeyArg_t k, unsigned int hash) { Assert(hash == m_hash(k)); return DoRemove(k, hash) >= 0; } // Alternate-type key removal, returns false if not found bool Remove(KeyAlt_t k) { return DoRemove(k, m_hash(k)) >= 0; } bool Remove(KeyAlt_t k, unsigned int hash) { Assert(hash == m_hash(k)); return DoRemove(k, hash) >= 0; } // Remove while iterating, returns the next handle for forward iteration // Note: aside from this, ALL handles are invalid if an element is removed handle_t RemoveAndAdvance(handle_t idx); // Nuke contents void RemoveAll(); // Nuke and release memory. void Purge() { RemoveAll(); m_table.Purge(); } // Reserve table capacity up front to avoid reallocation during insertions void Reserve(int expected) { if (expected > m_nUsed) DoRealloc(expected * 4 / 3); } // Shrink to best-fit size, re-insert keys for optimal lookup void Compact(bool bMinimal) { DoRealloc(bMinimal ? m_nUsed : (m_nUsed * 4 / 3)); } // Access functions. Note: if ValueT is empty_t, all functions return const // keys. typedef typename KVPair::ValueReturn_t Element_t; KeyT const &Key(handle_t idx) const { return m_table[idx]->m_key; } Element_t const &Element(handle_t idx) const { return m_table[idx]->GetValue(); } Element_t &Element(handle_t idx) { return m_table[idx]->GetValue(); } Element_t const &operator[](handle_t idx) const { return m_table[idx]->GetValue(); } Element_t &operator[](handle_t idx) { return m_table[idx]->GetValue(); } void ReplaceKey(handle_t idx, KeyArg_t k) { Assert(m_eq(m_table[idx]->m_key, k) && m_hash(k) == m_hash(m_table[idx]->m_key)); m_table[idx]->m_key = k; } void ReplaceKey(handle_t idx, KeyAlt_t k) { Assert(m_eq(m_table[idx]->m_key, k) && m_hash(k) == m_hash(m_table[idx]->m_key)); m_table[idx]->m_key = k; } Element_t const &Get(KeyArg_t k, Element_t const &defaultValue) const { handle_t h = Find(k); if (h != InvalidHandle()) return Element(h); return defaultValue; } Element_t const &Get(KeyAlt_t k, Element_t const &defaultValue) const { handle_t h = Find(k); if (h != InvalidHandle()) return Element(h); return defaultValue; } Element_t const *GetPtr(KeyArg_t k) const { handle_t h = Find(k); if (h != InvalidHandle()) return &Element(h); return NULL; } Element_t const *GetPtr(KeyAlt_t k) const { handle_t h = Find(k); if (h != InvalidHandle()) return &Element(h); return NULL; } Element_t *GetPtr(KeyArg_t k) { handle_t h = Find(k); if (h != InvalidHandle()) return &Element(h); return NULL; } Element_t *GetPtr(KeyAlt_t k) { handle_t h = Find(k); if (h != InvalidHandle()) return &Element(h); return NULL; } // Swap memory and contents with another identical hashtable // (NOTE: if using function pointers or functors with state, // it is up to the caller to ensure that they are compatible!) void Swap(CUtlHashtable &other) { m_table.Swap(other.m_table); ::V_swap(m_nUsed, other.m_nUsed); } #if _DEBUG // Validate the integrity of the hashtable void DbgCheckIntegrity() const; #endif private: CUtlHashtable(const CUtlHashtable ©ConstructorIsNotImplemented); }; // Set external memory (raw byte buffer, best-fit) template void CUtlHashtable::SetExternalBuffer(byte *pRawBuffer, unsigned int nBytes, bool bAssumeOwnership, bool bGrowable) { Assert(((uintptr_t)pRawBuffer % __alignof(int)) == 0); uint32 bestSize = LargestPowerOfTwoLessThanOrEqual(nBytes / sizeof(entry_t)); Assert(bestSize != 0 && bestSize * sizeof(entry_t) <= nBytes); return SetExternalBuffer((entry_t *)pRawBuffer, bestSize, bAssumeOwnership, bGrowable); } // Set external memory (typechecked, must be power of two) template void CUtlHashtable::SetExternalBuffer(entry_t *pBuffer, unsigned int nSize, bool bAssumeOwnership, bool bGrowable) { Assert(IsPowerOfTwo(nSize)); Assert(m_nUsed == 0); for (uint i = 0; i < nSize; ++i) pBuffer[i].MarkInvalid(); if (bAssumeOwnership) m_table.AssumeMemory(pBuffer, nSize); else m_table.SetExternalBuffer(pBuffer, nSize); m_bSizeLocked = !bGrowable; } // Allocate an empty table and then re-insert all existing entries. template void CUtlHashtable::DoRealloc( int size) { Assert(!m_bSizeLocked); size = SmallestPowerOfTwoGreaterOrEqual(MAX(m_nMinSize, size)); Assert(size > 0 && (uint)size <= entry_t::IdealIndex(~0, 0x1FFFFFFF)); // reasonable power of 2 Assert(size > m_nUsed); CUtlMemory oldTable; oldTable.Swap(m_table); entry_t *RESTRICT const pOldBase = oldTable.Base(); m_table.EnsureCapacity(size); entry_t *const pNewBase = m_table.Base(); for (int i = 0; i < size; ++i) pNewBase[i].MarkInvalid(); int nLeftToMove = m_nUsed; m_nUsed = 0; for (int i = oldTable.Count() - 1; i >= 0; --i) { if (pOldBase[i].IsValid()) { int newIdx = DoInsertUnconstructed(pOldBase[i].flags_and_hash, false); pNewBase[newIdx].MoveDataFrom(pOldBase[i]); if (--nLeftToMove == 0) break; } } Assert(nLeftToMove == 0); } // Move an existing entry to a free slot, leaving a hole behind template void CUtlHashtable::BumpEntry( unsigned int idx) { Assert(m_table[idx].IsValid()); Assert(m_nUsed < m_table.Count()); entry_t *table = m_table.Base(); unsigned int slotmask = m_table.Count() - 1; unsigned int new_flags_and_hash = table[idx].flags_and_hash & (FLAG_LAST | MASK_HASH); unsigned int chainid = entry_t::IdealIndex(new_flags_and_hash, slotmask); // Look for empty slots scanning forward, stripping FLAG_LAST as we go. // Note: this potentially strips FLAG_LAST from table[idx] if we pass it int newIdx = chainid; // start at ideal slot for (;; newIdx = (newIdx + 1) & slotmask) { if (table[newIdx].IdealIndex(slotmask) == chainid) { if (table[newIdx].flags_and_hash & FLAG_LAST) { table[newIdx].flags_and_hash &= ~FLAG_LAST; new_flags_and_hash |= FLAG_LAST; } continue; } if (table[newIdx].IsValid()) { continue; } break; } // Did we pick something closer to the ideal slot, leaving behind a // FLAG_LAST bit on the current slot because we didn't scan past it? if (table[idx].flags_and_hash & FLAG_LAST) { #ifdef _DEBUG Assert(new_flags_and_hash & FLAG_LAST); // Verify logic: we must have moved to an earlier slot, right? uint offset = ((uint)idx - chainid + slotmask + 1) & slotmask; uint newOffset = ((uint)newIdx - chainid + slotmask + 1) & slotmask; Assert(newOffset < offset); #endif // Scan backwards from old to new location, depositing FLAG_LAST on // the first match we find. (+slotmask) is the same as (-1) without // having to make anyone think about two's complement shenanigans. int scan = (idx + slotmask) & slotmask; while (scan != newIdx) { if (table[scan].IdealIndex(slotmask) == chainid) { table[scan].flags_and_hash |= FLAG_LAST; new_flags_and_hash &= ~FLAG_LAST; break; } scan = (scan + slotmask) & slotmask; } } // Move entry to the free slot we found, leaving a hole at idx table[newIdx].flags_and_hash = new_flags_and_hash; table[newIdx].MoveDataFrom(table[idx]); table[idx].MarkInvalid(); } // Insert a value at the root position for that value's hash chain. template int CUtlHashtable::DoInsertUnconstructed(unsigned int h, bool allowGrow) { if (allowGrow && !m_bSizeLocked) { // Keep the load factor between .25 and .75 int newSize = m_nUsed + 1; if ((newSize * 4 < m_table.Count() && m_table.Count() > m_nMinSize * 2) || newSize * 4 > m_table.Count() * 3) { DoRealloc(newSize * 4 / 3); } } Assert(m_nUsed < m_table.Count()); ++m_nUsed; entry_t *table = m_table.Base(); unsigned int slotmask = m_table.Count() - 1; unsigned int new_flags_and_hash = FLAG_LAST | (h & MASK_HASH); unsigned int idx = entry_t::IdealIndex(h, slotmask); if (table[idx].IdealIndex(slotmask) == idx) { // There is already an entry in this chain. new_flags_and_hash &= ~FLAG_LAST; BumpEntry(idx); } else if (table[idx].IsValid()) { // Somebody else is living in our ideal index but does not belong // to our entry chain; move it out of the way, start a new chain. BumpEntry(idx); } table[idx].flags_and_hash = new_flags_and_hash; return idx; } // Key lookup. Can also return previous-in-chain if result is a chained slot. template template UtlHashHandle_t CUtlHashtable::DoLookup( KeyParamT x, unsigned int h, handle_t *pPreviousInChain) const { if (m_nUsed == 0) { // Empty table. return (handle_t)-1; } const entry_t *table = m_table.Base(); unsigned int slotmask = m_table.Count() - 1; Assert(m_table.Count() > 0 && (slotmask & m_table.Count()) == 0); unsigned int chainid = entry_t::IdealIndex(h, slotmask); unsigned int idx = chainid; if (table[idx].IdealIndex(slotmask) != chainid) { // Nothing in root position? No match. return (handle_t)-1; } // Linear scan until found or end of chain handle_t lastIdx = (handle_t)-1; while (1) { // Only examine this slot if it is valid and belongs to our hash chain if (table[idx].IdealIndex(slotmask) == chainid) { // Test the full-width hash to avoid unnecessary calls to m_eq() if (((table[idx].flags_and_hash ^ h) & MASK_HASH) == 0 && m_eq(table[idx]->m_key, x)) { // Found match! if (pPreviousInChain) *pPreviousInChain = lastIdx; return (handle_t)idx; } if (table[idx].flags_and_hash & FLAG_LAST) { // End of chain. No match. return (handle_t)-1; } lastIdx = (handle_t)idx; } idx = (idx + 1) & slotmask; } } // Key insertion, or return index of existing key if found template template UtlHashHandle_t CUtlHashtable::DoInsert(KeyParamT k, unsigned int h) { handle_t idx = DoLookup(k, h, NULL); if (idx == (handle_t)-1) { idx = (handle_t)DoInsertUnconstructed(h, true); ConstructOneArg(m_table[idx].Raw(), k); } return idx; } // Key insertion, or return index of existing key if found template template UtlHashHandle_t CUtlHashtable::DoInsert( KeyParamT k, typename ArgumentTypeInfo::Arg_t v, unsigned int h, bool *pDidInsert) { handle_t idx = DoLookup(k, h, NULL); if (idx == (handle_t)-1) { idx = (handle_t)DoInsertUnconstructed(h, true); ConstructTwoArg(m_table[idx].Raw(), k, v); if (pDidInsert) *pDidInsert = true; } else { if (pDidInsert) *pDidInsert = false; } return idx; } // Key insertion template template UtlHashHandle_t CUtlHashtable::DoInsertNoCheck( KeyParamT k, typename ArgumentTypeInfo::Arg_t v, unsigned int h) { Assert(DoLookup(k, h, NULL) == (handle_t)-1); handle_t idx = (handle_t)DoInsertUnconstructed(h, true); ConstructTwoArg(m_table[idx].Raw(), k, v); return idx; } // Remove single element by key + hash. Returns the location of the new empty // hole. template template int CUtlHashtable::DoRemove( KeyParamT x, unsigned int h) { unsigned int slotmask = m_table.Count() - 1; handle_t previous = (handle_t)-1; int idx = (int)DoLookup(x, h, &previous); if (idx == -1) { return -1; } enum { FAKEFLAG_ROOT = 1 }; int nLastAndRootFlags = m_table[idx].flags_and_hash & FLAG_LAST; nLastAndRootFlags |= ((uint)idx == m_table[idx].IdealIndex(slotmask)); // Remove from table m_table[idx].MarkInvalid(); Destruct(m_table[idx].Raw()); --m_nUsed; if (nLastAndRootFlags == FLAG_LAST) // last only, not root { // This was the end of the chain - mark previous as last. // (This isn't the root, so there must be a previous.) Assert(previous != (handle_t)-1); m_table[previous].flags_and_hash |= FLAG_LAST; } if (nLastAndRootFlags == FAKEFLAG_ROOT) // root only, not last { // If we are removing the root and there is more to the chain, // scan to find the next chain entry and move it to the root. unsigned int chainid = entry_t::IdealIndex(h, slotmask); unsigned int nextIdx = idx; while (1) { nextIdx = (nextIdx + 1) & slotmask; if (m_table[nextIdx].IdealIndex(slotmask) == chainid) { break; } } Assert(!(m_table[nextIdx].flags_and_hash & FLAG_FREE)); // Leave a hole where the next entry in the chain was. m_table[idx].flags_and_hash = m_table[nextIdx].flags_and_hash; m_table[idx].MoveDataFrom(m_table[nextIdx]); m_table[nextIdx].MarkInvalid(); return nextIdx; } // The hole is still where the element used to be. return idx; } // Assignment operator. It's up to the user to make sure that the hash and // equality functors match. template CUtlHashtable &CUtlHashtable::operator=( CUtlHashtable const &src) { if (&src != this) { Assert(!m_bSizeLocked || m_table.Count() >= src.m_nUsed); if (!m_bSizeLocked) { Purge(); Reserve(src.m_nUsed); } else { RemoveAll(); } const entry_t *srcTable = src.m_table.Base(); for (int i = src.m_table.Count() - 1; i >= 0; --i) { if (srcTable[i].IsValid()) { // If this assert trips, double-check that both hashtables // have the same hash function pointers or hash functor state! Assert(m_hash(srcTable[i]->m_key) == src.m_hash(srcTable[i]->m_key)); int newIdx = DoInsertUnconstructed(srcTable[i].flags_and_hash, false); CopyConstruct(m_table[newIdx].Raw(), *srcTable[i].Raw()); // copy construct KVPair } } } return *this; } // Remove and return the next valid iterator for a forward iteration. template UtlHashHandle_t CUtlHashtable::RemoveAndAdvance(UtlHashHandle_t idx) { Assert(IsValidHandle(idx)); // TODO optimize, implement DoRemoveAt that does not need to re-evaluate // equality in DoLookup int hole = DoRemove(m_table[idx]->m_key, m_table[idx].flags_and_hash & MASK_HASH); // DoRemove returns the index of the element that it moved to fill the hole, // if any. if (hole <= (int)idx) { // Didn't fill, or filled from a previously seen element. return NextHandle(idx); } else { // Do not advance; slot has a new un-iterated value. Assert(IsValidHandle(idx)); return idx; } } // Burn it with fire. template void CUtlHashtable::RemoveAll() { int used = m_nUsed; if (used != 0) { entry_t *table = m_table.Base(); for (int i = m_table.Count() - 1; i >= 0; --i) { if (table[i].IsValid()) { table[i].MarkInvalid(); Destruct(table[i].Raw()); if (--used == 0) break; } } m_nUsed = 0; } } template UtlHashHandle_t CUtlHashtable::NextHandle(handle_t start) const { const entry_t *table = m_table.Base(); for (int i = (int)start + 1; i < m_table.Count(); ++i) { if (table[i].IsValid()) return (handle_t)i; } return (handle_t)-1; } #if _DEBUG template void CUtlHashtable::DbgCheckIntegrity() const { // Stress test the hash table as a test of both container functionality // and also the validity of the user's Hash and Equal function objects. // NOTE: will fail if function objects require any sort of state! CUtlHashtable clone; unsigned int bytes = sizeof(entry_t) * max(16, m_table.Count()); byte *tempbuf = (byte *)malloc(bytes); clone.SetExternalBuffer(tempbuf, bytes, false, false); clone = *this; int count = 0, roots = 0, ends = 0; int slotmask = m_table.Count() - 1; for (int i = 0; i < m_table.Count(); ++i) { if (!(m_table[i].flags_and_hash & FLAG_FREE)) ++count; if (m_table[i].IdealIndex(slotmask) == (uint)i) ++roots; if (m_table[i].flags_and_hash & FLAG_LAST) ++ends; if (m_table[i].IsValid()) { Assert(Find(m_table[i]->m_key) == (handle_t)i); Verify(clone.Remove(m_table[i]->m_key)); } else { Assert(m_table[i].flags_and_hash == FLAG_FREE); } } Assert(count == Count() && count >= roots && roots == ends); Assert(clone.Count() == 0); clone.Purge(); free(tempbuf); } #endif //----------------------------------------------------------------------- // CUtlStableHashtable //----------------------------------------------------------------------- // Stable hashtables are less memory and cache efficient, but can be // iterated quickly and their element handles are completely stable. // Implemented as a hashtable which only stores indices, and a separate // CUtlLinkedList data table which contains key-value pairs; this may // change to a more efficient structure in the future if space becomes // critical. I have some ideas about that but not the time to implement // at the moment. -henryg // Note: RemoveAndAdvance is slower than in CUtlHashtable because the // key needs to be re-hashed under the current implementation. template , typename KeyIsEqualT = DefaultEqualFunctor, typename IndexStorageT = uint16, typename AlternateKeyT = typename ArgumentTypeInfo::Alt_t> class CUtlStableHashtable { public: typedef typename ArgumentTypeInfo::Arg_t KeyArg_t; typedef typename ArgumentTypeInfo::Arg_t ValueArg_t; typedef typename ArgumentTypeInfo::Arg_t KeyAlt_t; typedef typename CTypeSelect::type IndexStorage_t; protected: COMPILE_TIME_ASSERT(sizeof(IndexStorage_t) == sizeof(IndexStorageT)); typedef CUtlKeyValuePair KVPair; struct HashProxy; struct EqualProxy; struct IndirectIndex; typedef CUtlHashtable Hashtable_t; typedef CUtlLinkedList LinkedList_t; template bool DoRemove(KeyArgumentT k); template UtlHashHandle_t DoFind(KeyArgumentT k) const; template UtlHashHandle_t DoInsert(KeyArgumentT k); template UtlHashHandle_t DoInsert(KeyArgumentT k, ValueArgumentT v); public: KeyHashT &GetHashRef() { return m_table.GetHashRef().m_hash; } KeyIsEqualT &GetEqualRef() { return m_table.GetEqualRef().m_eq; } KeyHashT const &GetHashRef() const { return m_table.GetHashRef().m_hash; } KeyIsEqualT const &GetEqualRef() const { return m_table.GetEqualRef().m_eq; } UtlHashHandle_t Insert(KeyArg_t k) { return DoInsert(k); } UtlHashHandle_t Insert(KeyAlt_t k) { return DoInsert(k); } UtlHashHandle_t Insert(KeyArg_t k, ValueArg_t v) { return DoInsert(k, v); } UtlHashHandle_t Insert(KeyAlt_t k, ValueArg_t v) { return DoInsert(k, v); } UtlHashHandle_t Find(KeyArg_t k) const { return DoFind(k); } UtlHashHandle_t Find(KeyAlt_t k) const { return DoFind(k); } bool Remove(KeyArg_t k) { return DoRemove(k); } bool Remove(KeyAlt_t k) { return DoRemove(k); } void RemoveAll() { m_table.RemoveAll(); m_data.RemoveAll(); } void Purge() { m_table.Purge(); m_data.Purge(); } int Count() const { return m_table.Count(); } typedef typename KVPair::ValueReturn_t Element_t; KeyT const &Key(UtlHashHandle_t idx) const { return m_data[idx].m_key; } Element_t const &Element(UtlHashHandle_t idx) const { return m_data[idx].GetValue(); } Element_t &Element(UtlHashHandle_t idx) { return m_data[idx].GetValue(); } Element_t const &operator[](UtlHashHandle_t idx) const { return m_data[idx].GetValue(); } Element_t &operator[](UtlHashHandle_t idx) { return m_data[idx].GetValue(); } void ReplaceKey(UtlHashHandle_t idx, KeyArg_t k) { Assert(GetEqualRef()(m_data[idx].m_key, k) && GetHashRef()(k) == GetHashRef()(m_data[idx].m_key)); m_data[idx].m_key = k; } void ReplaceKey(UtlHashHandle_t idx, KeyAlt_t k) { Assert(GetEqualRef()(m_data[idx].m_key, k) && GetHashRef()(k) == GetHashRef()(m_data[idx].m_key)); m_data[idx].m_key = k; } Element_t const &Get(KeyArg_t k, Element_t const &defaultValue) const { UtlHashHandle_t h = Find(k); if (h != InvalidHandle()) return Element(h); return defaultValue; } Element_t const &Get(KeyAlt_t k, Element_t const &defaultValue) const { UtlHashHandle_t h = Find(k); if (h != InvalidHandle()) return Element(h); return defaultValue; } Element_t const *GetPtr(KeyArg_t k) const { UtlHashHandle_t h = Find(k); if (h != InvalidHandle()) return &Element(h); return NULL; } Element_t const *GetPtr(KeyAlt_t k) const { UtlHashHandle_t h = Find(k); if (h != InvalidHandle()) return &Element(h); return NULL; } Element_t *GetPtr(KeyArg_t k) { UtlHashHandle_t h = Find(k); if (h != InvalidHandle()) return &Element(h); return NULL; } Element_t *GetPtr(KeyAlt_t k) { UtlHashHandle_t h = Find(k); if (h != InvalidHandle()) return &Element(h); return NULL; } UtlHashHandle_t FirstHandle() const { return ExtendInvalidHandle(m_data.Head()); } UtlHashHandle_t NextHandle(UtlHashHandle_t h) const { return ExtendInvalidHandle(m_data.Next(h)); } bool IsValidHandle(UtlHashHandle_t h) const { return m_data.IsValidIndex(h); } UtlHashHandle_t InvalidHandle() const { return (UtlHashHandle_t)-1; } UtlHashHandle_t RemoveAndAdvance(UtlHashHandle_t h) { Assert(m_data.IsValidIndex(h)); m_table.Remove(IndirectIndex(h)); IndexStorage_t next = m_data.Next(h); m_data.Remove(h); return ExtendInvalidHandle(next); } void Compact(bool bMinimal) { m_table.Compact(bMinimal); /*m_data.Compact();*/ } void Swap(CUtlStableHashtable &other) { m_table.Swap(other.m_table); // XXX swapping CUtlLinkedList by block memory swap, ugh char buf[sizeof(m_data)]; memcpy(buf, &m_data, sizeof(m_data)); memcpy(&m_data, &other.m_data, sizeof(m_data)); memcpy(&other.m_data, buf, sizeof(m_data)); } protected: // Perform extension of 0xFFFF to 0xFFFFFFFF if necessary. Note: ( a < // CONSTANT ) ? 0 : -1 is usually branchless static UtlHashHandle_t ExtendInvalidHandle(uint32 x) { return x; } static UtlHashHandle_t ExtendInvalidHandle(uint16 x) { uint32 a = x; return a | ((a < 0xFFFFu) ? 0 : -1); } struct IndirectIndex { explicit IndirectIndex(IndexStorage_t i) : m_index(i) {} IndexStorage_t m_index; }; struct HashProxy { KeyHashT m_hash; unsigned int operator()(IndirectIndex idx) const { const ptrdiff_t tableoffset = (uintptr_t)(&((Hashtable_t *)1024)->GetHashRef()) - 1024; const ptrdiff_t owneroffset = offsetof(CUtlStableHashtable, m_table) + tableoffset; CUtlStableHashtable *pOwner = (CUtlStableHashtable *)((uintptr_t)this - owneroffset); return m_hash(pOwner->m_data[idx.m_index].m_key); } unsigned int operator()(KeyArg_t k) const { return m_hash(k); } unsigned int operator()(KeyAlt_t k) const { return m_hash(k); } }; struct EqualProxy { KeyIsEqualT m_eq; unsigned int operator()(IndirectIndex lhs, IndirectIndex rhs) const { return lhs.m_index == rhs.m_index; } unsigned int operator()(IndirectIndex lhs, KeyArg_t rhs) const { const ptrdiff_t tableoffset = (uintptr_t)(&((Hashtable_t *)1024)->GetEqualRef()) - 1024; const ptrdiff_t owneroffset = offsetof(CUtlStableHashtable, m_table) + tableoffset; CUtlStableHashtable *pOwner = (CUtlStableHashtable *)((uintptr_t)this - owneroffset); return m_eq(pOwner->m_data[lhs.m_index].m_key, rhs); } unsigned int operator()(IndirectIndex lhs, KeyAlt_t rhs) const { const ptrdiff_t tableoffset = (uintptr_t)(&((Hashtable_t *)1024)->GetEqualRef()) - 1024; const ptrdiff_t owneroffset = offsetof(CUtlStableHashtable, m_table) + tableoffset; CUtlStableHashtable *pOwner = (CUtlStableHashtable *)((uintptr_t)this - owneroffset); return m_eq(pOwner->m_data[lhs.m_index].m_key, rhs); } }; class CCustomLinkedList : public LinkedList_t { public: int AddToTailUnconstructed() { IndexStorage_t newNode = this->AllocInternal(); if (newNode != this->InvalidIndex()) this->LinkToTail(newNode); return newNode; } }; Hashtable_t m_table; CCustomLinkedList m_data; }; template template inline bool CUtlStableHashtable::DoRemove(KeyArgumentT k) { unsigned int hash = m_table.GetHashRef()(k); UtlHashHandle_t h = m_table.template DoLookup(k, hash, NULL); if (h == m_table.InvalidHandle()) return false; int idx = m_table[h].m_index; m_table.template DoRemove(IndirectIndex(idx), hash); m_data.Remove(idx); return true; } template template inline UtlHashHandle_t CUtlStableHashtable::DoFind( KeyArgumentT k) const { unsigned int hash = m_table.GetHashRef()(k); UtlHashHandle_t h = m_table.template DoLookup(k, hash, NULL); if (h != m_table.InvalidHandle()) return m_table[h].m_index; return (UtlHashHandle_t)-1; } template template inline UtlHashHandle_t CUtlStableHashtable::DoInsert( KeyArgumentT k) { unsigned int hash = m_table.GetHashRef()(k); UtlHashHandle_t h = m_table.template DoLookup(k, hash, NULL); if (h != m_table.InvalidHandle()) return m_table[h].m_index; int idx = m_data.AddToTailUnconstructed(); ConstructOneArg(&m_data[idx], k); m_table.template DoInsertNoCheck(IndirectIndex(idx), empty_t(), hash); return idx; } template template inline UtlHashHandle_t CUtlStableHashtable::DoInsert( KeyArgumentT k, ValueArgumentT v) { unsigned int hash = m_table.GetHashRef()(k); UtlHashHandle_t h = m_table.template DoLookup(k, hash, NULL); if (h != m_table.InvalidHandle()) return m_table[h].m_index; int idx = m_data.AddToTailUnconstructed(); ConstructTwoArg(&m_data[idx], k, v); m_table.template DoInsertNoCheck(IndirectIndex(idx), empty_t(), hash); return idx; } #endif // UTLHASHTABLE_H