//========= Copyright Valve Corporation, All rights reserved. ============// // // Purpose: // // $NoKeywords: $ //============================================================================= #ifndef THASH_H #define THASH_H #ifdef _WIN32 #pragma once #endif #include //#define DBGFLAG_THASH // Perform extra sanity checks on the //THash // THash // This is a heavyweight, templatized version of DHash. // It differs from DHash in the following ways: // - It's templetized, and automatically constructs and destructs its records as // appropriate // - It provides a scheduling service, which can be used to touch every record // in the table // at a specified interval. The scheduler is low-overhead, and //provides a very smooth distribution of touches across frames. // Template arguments: // Data: the class to be stored in the hash table // I: the type of the primary key (uint32 by default) template class CTHash { private: // RecHdr // We insert one of these at the beginning of every record. It's used for // keeping the records in a linked list. typedef struct RecHdr_t { RecHdr_t *m_pRecHdrNext; // Next item in our linked list RecHdr_t *m_pRecHdrPrev; // Previous item in our linked list I m_unKey; // Key of this item int m_iBucket; // The bucket we're in int m_nRunRatio; // We want to run 1 cycle out of every m_nRunRatio // cycles (not at all if 0). #ifdef DBGFLAG_THASH uint m_iCycleLast; // Last cycle we were visited (whether or not we ran) #endif } RecHdr_t; // Bucket // Each hash bucket is represented by a Bucket structure, which points to // the first record with the bucket's hash. typedef struct Bucket_t { RecHdr_t *m_pRecHdrFirst; // First record in our list } Bucket_t; public: // Constructors & destructors CTHash(int cFramesPerCycle); ~CTHash(); // Initializer void Init(int cRecordInit, int cBuckets); // Insert a record into the table Data *PvRecordInsert(I unKey); // Insert a record into the table and set the allocated object's pointer as // the hash key Data *PvRecordInsertAutoKey(); // Changes the key for a previously inserted item void ChangeKey(Data *pvRecord, I unOldKey, I unNewKey); // Remove a record void Remove(I unKey); // Remove a record void Remove(Data *pvRecord); // Remove all records void RemoveAll(); // Find a record Data *PvRecordFind(I unKey) const; // How many records do we have int Count() const { return m_cRecordInUse; } // Iterate through our members Data *PvRecordFirst() const; Data *PvRecordNext(Data *pvRecordCur) const; // We provide a scheduling service. Call StartFrameSchedule when you want // to start running records in a given frame, and repeatedly call // PvRecordRun until it returns NULL (or you run our of time). void SetRunRatio(Data *pvRecord, int nRunRatio); void SetMicroSecPerCycle(int cMicroSecPerCycle, int cMicroSecPerFrame) { m_cFramesPerCycle = cMicroSecPerCycle / cMicroSecPerFrame; } void StartFrameSchedule(bool bNewFrame); Data *PvRecordRun(); bool BCompletedPass(); #ifdef DBGFLAG_VALIDATE virtual void Validate(CValidator &validator, const char *pchName); #endif // DBGFLAG_VALIDATE private: // Insert a record into the table Data *PvRecordInsertInternal(RecHdr_t *pRecHdr, I unKey); // Get the record associated with a THashHdr Data *PvRecordFromPRecHdr(RecHdr_t *pRecHdr) const { return (Data *)(((uint8 *)pRecHdr + sizeof(RecHdr_t))); } // Get the RecHdr preceding a PvRecord RecHdr_t *PRecHdrFromPvRecord(Data *pvRecord) const { return (((RecHdr_t *)pvRecord) - 1); } // Get the hash bucket corresponding to a key int IBucket(I unKey) const; void InsertIntoHash(RecHdr_t *pRecHdr, I unKey); void RemoveFromHash(Data *pvRecord); int m_cBucket; // # of hash buckets we have Bucket_t *m_pBucket; // Big array of hash buckets CUtlMemoryPool *m_pMemoryPoolRecord; // All our data records int m_cRecordInUse; // # of records in use RecHdr_t m_RecHdrHead; // Head of our linked list RecHdr_t m_RecHdrTail; // Tail of our linked list int m_cFramesPerCycle; // Run each of our records once every // m_cFramesPerCycle frames RecHdr_t *m_pRecHdrRunNext; // Next record to run (be careful-- this is // more complicated than it sounds) int m_iBucketRunMax; // Stop running when we get to this bucket uint m_iCycleCur; // Current cycle (ie, how many times we've made a // complete scheduler pass) uint m_iCycleLast; // Our previous cycle uint m_iFrameCur; // Our current frame (incremented once each // StartFrameSchedule) uint m_iCycleLastReported; // Last cycle checked by BCompletedPass() }; //----------------------------------------------------------------------------- // Purpose: Constructor // Input: cMicroSecRunInterval - How often we want the scheduler to // run each of our records //----------------------------------------------------------------------------- template CTHash::CTHash(int cFramesPerCycle) { m_cBucket = 0; m_pBucket = NULL; m_pMemoryPoolRecord = NULL; m_cRecordInUse = 0; m_cFramesPerCycle = cFramesPerCycle; m_pRecHdrRunNext = &m_RecHdrTail; // This will make us start at the // beginning on our first frame m_iBucketRunMax = 0; m_iCycleCur = 0; m_iCycleLast = 0; m_iFrameCur = 0; m_iCycleLastReported = 0; m_RecHdrHead.m_pRecHdrPrev = NULL; m_RecHdrHead.m_pRecHdrNext = &m_RecHdrTail; m_RecHdrHead.m_iBucket = -1; m_RecHdrTail.m_pRecHdrPrev = &m_RecHdrHead; m_RecHdrTail.m_pRecHdrNext = NULL; } //----------------------------------------------------------------------------- // Purpose: Destructor //----------------------------------------------------------------------------- template CTHash::~CTHash() { RemoveAll(); if (NULL != m_pBucket) FreePv(m_pBucket); m_pBucket = NULL; if (NULL != m_pMemoryPoolRecord) delete (m_pMemoryPoolRecord); m_pMemoryPoolRecord = NULL; } //----------------------------------------------------------------------------- // Purpose: Initializer. Allocate our various arrays, and set up the free // list. // Input: cRecordInit - Initial # of data records we can contain // cBucket - # of hash buckets we should //use //----------------------------------------------------------------------------- template void CTHash::Init(int cRecordInit, int cBucket) { Assert( cRecordInit > 0); // need to init with non-zero value or memory pool will never grow // Copy our parameters m_cBucket = cBucket; // Alloc our arrays m_pBucket = (Bucket_t *)PvAlloc(sizeof(Bucket_t) * m_cBucket); m_pMemoryPoolRecord = new CUtlMemoryPool(sizeof(Data) + sizeof(RecHdr_t), cRecordInit, CUtlMemoryPool::GROW_SLOW); // Init the hash buckets for (int iBucket = 0; iBucket < m_cBucket; iBucket++) m_pBucket[iBucket].m_pRecHdrFirst = NULL; // Make the tail have an illegally large bucket m_RecHdrTail.m_iBucket = m_cBucket + 1; } //----------------------------------------------------------------------------- // Purpose: Inserts a new record into the table // Input: unKey - Primary key of the new record // Output: Pointer to the new record //----------------------------------------------------------------------------- template Data *CTHash::PvRecordInsert(I unKey) { Assert(PvRecordFind(unKey) == NULL); // keys are unique; no record with this key may exist // Find a free record RecHdr_t *pRecHdr = (RecHdr_t *)m_pMemoryPoolRecord->Alloc(); return PvRecordInsertInternal(pRecHdr, unKey); } //----------------------------------------------------------------------------- // Purpose: Inserts a new record into the table and sets its key to the pointer // value of the record // Output: Pointer to the new record //----------------------------------------------------------------------------- template Data *CTHash::PvRecordInsertAutoKey() { // Find a free record RecHdr_t *pRecHdr = (RecHdr_t *)m_pMemoryPoolRecord->Alloc(); return PvRecordInsertInternal(pRecHdr, (I)PvRecordFromPRecHdr(pRecHdr)); } //----------------------------------------------------------------------------- // Purpose: Inserts an allocated record into the hash table with specified key // and calls the constructor of the allocated object // Input: pRecHdr - record to insert // unKey - hash key to use for record // Output: Pointer to the new record //----------------------------------------------------------------------------- template Data *CTHash::PvRecordInsertInternal(RecHdr_t *pRecHdr, I unKey) { InsertIntoHash(pRecHdr, unKey); // assert that we don't have too many items per bucket static bool s_bPerfWarning = false; if (!s_bPerfWarning && Count() >= (5 * m_cBucket)) { s_bPerfWarning = true; AssertMsg(false, "Performance warning: too many items, not enough buckets"); Msg("not enough buckets in thash class %s (%d records, %d buckets)\n", #ifdef _WIN32 typeid(*this).raw_name(), #else typeid(*this).name(), #endif Count(), m_cBucket); } // Construct ourselves Data *pData = PvRecordFromPRecHdr(pRecHdr); Construct(pData); return pData; } //----------------------------------------------------------------------------- // Purpose: Changes key on previously inserted item // Input: pvRecord - record to change key for // unOldKey - old key (not strictly needed, but //helpful consistency check) unNewKey - new key to use //----------------------------------------------------------------------------- template void CTHash::ChangeKey(Data *pvRecord, I unOldKey, I unNewKey) { Data *pvRecordFound = PvRecordFind(unOldKey); Assert(pvRecordFound == pvRecord); if (pvRecordFound == pvRecord) { RemoveFromHash(pvRecord); InsertIntoHash(PRecHdrFromPvRecord(pvRecord), unNewKey); } } //----------------------------------------------------------------------------- // Purpose: Removes the entry with a specified key from the table // Input: unKey - Key of the entry to remove //----------------------------------------------------------------------------- template void CTHash::Remove(I unKey) { Data *pvRemove = (Data *)PvRecordFind(unKey); Assert(pvRemove); if (!pvRemove) return; Remove(pvRemove); } //----------------------------------------------------------------------------- // Purpose: Removes the specified entry from the table // Input: pvRemove - Pointer to the entry to remove //----------------------------------------------------------------------------- template void CTHash::Remove(Data *pvRemove) { // Destruct the record we're removing Destruct(pvRemove); RemoveFromHash(pvRemove); m_pMemoryPoolRecord->Free(PRecHdrFromPvRecord(pvRemove)); } //----------------------------------------------------------------------------- // Purpose: Removes all entries from the table //----------------------------------------------------------------------------- template void CTHash::RemoveAll() { Data *pvRecord = PvRecordFirst(); while (pvRecord) { Data *pvRecordNext = PvRecordNext(pvRecord); Remove(pvRecord); pvRecord = pvRecordNext; } } //----------------------------------------------------------------------------- // Purpose: Finds the entry with a specified key // Input: unKey - Key to find //----------------------------------------------------------------------------- template Data *CTHash::PvRecordFind(I unKey) const { // Find our hash bucket int iBucket = IBucket(unKey); // Walk the bucket's list looking for an exact match for (RecHdr_t *pRecHdr = m_pBucket[iBucket].m_pRecHdrFirst; NULL != pRecHdr && pRecHdr->m_iBucket == iBucket; pRecHdr = pRecHdr->m_pRecHdrNext) { if (unKey == pRecHdr->m_unKey) return PvRecordFromPRecHdr(pRecHdr); } // Didn't find a match return NULL; } //----------------------------------------------------------------------------- // Purpose: Finds our first record // Output: Pointer to our first record //----------------------------------------------------------------------------- template Data *CTHash::PvRecordFirst() const { if (&m_RecHdrTail != m_RecHdrHead.m_pRecHdrNext) return PvRecordFromPRecHdr(m_RecHdrHead.m_pRecHdrNext); else return NULL; } //----------------------------------------------------------------------------- // Purpose: Iterates to the record after a given record // Input: Pointer to a current record // Output: Pointer to the next record //----------------------------------------------------------------------------- template Data *CTHash::PvRecordNext(Data *pvRecordCur) const { RecHdr_t *pRecHdr = PRecHdrFromPvRecord(pvRecordCur); if (&m_RecHdrTail == pRecHdr->m_pRecHdrNext) return NULL; return PvRecordFromPRecHdr(pRecHdr->m_pRecHdrNext); } //----------------------------------------------------------------------------- // Purpose: Sets the run ratio of a particular record in the hash table. // The record will be run 1 cycle out of every nRunRatio //cycles. // Input: pvRecord - The record we're setting // nRunRatio - The run ratio for this record //----------------------------------------------------------------------------- template void CTHash::SetRunRatio(Data *pvRecord, int nRunRatio) { PRecHdrFromPvRecord(pvRecord)->m_nRunRatio = nRunRatio; } //----------------------------------------------------------------------------- // Purpose: Prepares us to run all records that are due to be run this frame. // Records are run at a particular time dependent on their hash //bucket, regardless of when they were last run. // Input: bNewFrame - True if this is a new frame. If false, we've // run // off the end of the list and are //checking whether we need to keep going at the beginning. //----------------------------------------------------------------------------- template void CTHash::StartFrameSchedule(bool bNewFrame) { // Calculate our current frame and cycle cycle if (bNewFrame) { m_iCycleLast = m_iCycleCur; m_iFrameCur++; m_iCycleCur = m_iFrameCur / m_cFramesPerCycle; } // Calculate the last bucket to run int iFrameInCycle = m_iFrameCur % m_cFramesPerCycle; m_iBucketRunMax = (int)(((int64)(iFrameInCycle + 1) * (int64)m_cBucket) / (int64)m_cFramesPerCycle); AssertFatal(m_iBucketRunMax >= 0 && m_iBucketRunMax <= m_cBucket); // Are we starting a new cycle? if (m_iCycleCur > m_iCycleLast) { #ifdef DBGFLAG_THASH Assert(m_iCycleCur == m_iCycleLast + 1); #endif // Did we finish the last cycle? if (&m_RecHdrTail == m_pRecHdrRunNext) { m_pRecHdrRunNext = m_RecHdrHead.m_pRecHdrNext; } // No-- finish it up before moving on else { m_iBucketRunMax = m_cBucket + 1; } } } //----------------------------------------------------------------------------- // Purpose: Returns the next record to run, if any // Output: Pointer to the next record that needs to run (NULL if we're // done) //----------------------------------------------------------------------------- template Data *CTHash::PvRecordRun() { // Loop until we find a record to run, or until we pass m_iBucketRunMax for (;;) { // Are we past our stopping point? if (m_pRecHdrRunNext->m_iBucket >= m_iBucketRunMax) { // If this cycle ran to the very end, see if we need to start over if (m_iBucketRunMax > m_cBucket) { StartFrameSchedule(false); continue; } return NULL; } #ifdef DBGFLAG_THASH Assert(m_pRecHdrRunNext->m_iBucket >= m_iBucketRunFirst); if (0 != m_pRecHdrRunNext->m_iCycleLast) { if (m_pRecHdrRunNext->m_iCycleLast == m_iCycleCur) { DMsg(SPEW_CONSOLE, 1, "Double cycle: hdr = 0x%x, last frame = %d, curFrame = " "%d, first = %d, last = %d, bucket = %d\n", m_pRecHdrRunNext, m_pRecHdrRunNext->m_iFrameLast, m_iFrame, m_iBucketRunFirst, m_iBucketRunMax, m_pRecHdrRunNext->m_iBucket); } else if (m_pRecHdrRunNext->m_iCycleLast != m_iCycleCur - 1) { DMsg(SPEW_CONSOLE, 1, "Skipped cycle: hdr = 0x%x, cycleLast = %u, cycleCur = %u " "(missed %u cycles)\n", m_pRecHdrRunNext, m_pRecHdrRunNext->m_iCycleLast, m_iCycleCur, m_iCycleCur - m_pRecHdrRunNext->m_iCycleLast); Assert(false); } } m_pRecHdrRunNext->m_iCycleLast = m_iCycleCur; m_pRecHdrRunNext->m_iFrameLast = m_iFrame; #endif // Set up the record to run next time RecHdr_t *pRecHdrCur = m_pRecHdrRunNext; m_pRecHdrRunNext = m_pRecHdrRunNext->m_pRecHdrNext; // Does this record need to run? if (0 == pRecHdrCur->m_nRunRatio) continue; if (0 == m_iCycleCur % pRecHdrCur->m_nRunRatio) return PvRecordFromPRecHdr(pRecHdrCur); } } //----------------------------------------------------------------------------- // Purpose: Return true if we've completed a scheduler pass since last called //----------------------------------------------------------------------------- template bool CTHash::BCompletedPass() { if (m_iCycleCur != m_iCycleLastReported) { m_iCycleLastReported = m_iCycleCur; return true; } return false; } extern const unsigned char g_CTHashRandomValues[256]; // definition lives in globals.cpp //----------------------------------------------------------------------------- // Purpose: Returns the index of the hash bucket corresponding to a particular // key Input: unKey - Key to find Output: Index of the hash bucket // corresponding to unKey //----------------------------------------------------------------------------- template int CTHash::IBucket(I unKey) const { AssertFatal(m_cBucket > 0); // This is a pearsons hash variant that returns a maximum of 32 bits size_t size = sizeof(I); const uint8 *k = (const uint8 *)&unKey; uint32 byte_one = 0, byte_two = 0, byte_three = 0, byte_four = 0, n; while (size) { --size; n = *k++; byte_one = g_CTHashRandomValues[byte_one ^ n]; if (size) { --size; n = *k++; byte_two = g_CTHashRandomValues[byte_two ^ n]; } else break; if (size) { --size; n = *k++; byte_three = g_CTHashRandomValues[byte_three ^ n]; } else break; if (size) { --size; n = *k++; byte_four = g_CTHashRandomValues[byte_four ^ n]; } else break; } uint32 idx = (byte_four << 24) | (byte_three << 16) | (byte_two << 8) | byte_one; idx = idx % m_cBucket; return ((int)idx); } #ifdef DBGFLAG_VALIDATE //----------------------------------------------------------------------------- // Purpose: Run a global validation pass on all of our data structures and // memory // allocations. // Input: validator - Our global validator object // pchName - Our name (typically a member var in our //container) //----------------------------------------------------------------------------- template void CTHash::Validate(CValidator &validator, const char *pchName) { VALIDATE_SCOPE(); validator.ClaimMemory(m_pBucket); ValidatePtr(m_pMemoryPoolRecord); #if defined(_DEBUG) // first verify m_cRecordInUse Data *pvRecord = PvRecordFirst(); int cItems = 0; while (pvRecord) { Data *pvRecordNext = PvRecordNext(pvRecord); cItems++; pvRecord = pvRecordNext; } Assert(m_cRecordInUse == cItems); // then ask the mempool to verify this if (m_pMemoryPoolRecord) m_pMemoryPoolRecord->LeakCheck(cItems); #endif // _DEBUG } #endif // DBGFLAG_VALIDATE //----------------------------------------------------------------------------- // Purpose: Inserts a new record into the table // Input: unKey - Primary key of the new record // Output: Pointer to the new record //----------------------------------------------------------------------------- template void CTHash::InsertIntoHash(RecHdr_t *pRecHdr, I unKey) { m_cRecordInUse++; // Init the RecHdr pRecHdr->m_unKey = unKey; pRecHdr->m_nRunRatio = 1; // Find our hash bucket int iBucket = IBucket(unKey); pRecHdr->m_iBucket = iBucket; #ifdef DBGFLAG_THASH pRecHdr->m_iCycleLast = 0; #endif // Find where to insert ourselves in the linked list RecHdr_t *pRecHdrInsertBefore = &m_RecHdrTail; // Find the first bucket with anything in it that's at or after our bucket for (int iBucketT = iBucket; iBucketT < m_cBucket; iBucketT++) { if (NULL != m_pBucket[iBucketT].m_pRecHdrFirst) { pRecHdrInsertBefore = m_pBucket[iBucketT].m_pRecHdrFirst; break; } } // Insert ourselves pRecHdr->m_pRecHdrNext = pRecHdrInsertBefore; pRecHdr->m_pRecHdrPrev = pRecHdrInsertBefore->m_pRecHdrPrev; pRecHdrInsertBefore->m_pRecHdrPrev = pRecHdr; pRecHdr->m_pRecHdrPrev->m_pRecHdrNext = pRecHdr; // Our bucket should point to us m_pBucket[iBucket].m_pRecHdrFirst = pRecHdr; } //----------------------------------------------------------------------------- // Purpose: Removes the specified entry from the table // Input: pvRemove - Pointer to the entry to remove //----------------------------------------------------------------------------- template void CTHash::RemoveFromHash(Data *pvRemove) { // Find our RecHdr RecHdr_t *pRecHdr = PRecHdrFromPvRecord(pvRemove); // If our bucket points to us, point it to the next record (or else NULL) int iBucket = IBucket(pRecHdr->m_unKey); if (pRecHdr == m_pBucket[iBucket].m_pRecHdrFirst) { if (pRecHdr->m_pRecHdrNext->m_iBucket == iBucket) m_pBucket[iBucket].m_pRecHdrFirst = pRecHdr->m_pRecHdrNext; else m_pBucket[iBucket].m_pRecHdrFirst = NULL; } // Remove us from the linked list pRecHdr->m_pRecHdrPrev->m_pRecHdrNext = pRecHdr->m_pRecHdrNext; pRecHdr->m_pRecHdrNext->m_pRecHdrPrev = pRecHdr->m_pRecHdrPrev; // Are we the next record to run? if (pRecHdr == m_pRecHdrRunNext) m_pRecHdrRunNext = pRecHdr->m_pRecHdrNext; m_cRecordInUse--; } #endif // THASH_H