Topic 03: Progressive Rehashing & Amortized O(1) Hash Table

Target Duration: 2โ€“4 minutes (~300โ€“450 spoken words)
Focus: Pointwise verbal delivery covering fixed-size hash table slots, power-of-two masking, load factor triggers, dual-table incremental migration (k_rehashing_work = 128), and double-pointer deletion mechanics.


๐ŸŽ™๏ธ Pointwise Spoken Speech (Word-for-Word Delivery)


๐Ÿ“‹ Step-by-Step Summary (What, How & Why)

Step What Was Done How It Works Why This Mechanism / Order Code Reference
1. Fast Indexing Used power-of-two table capacity Computes slot index as pos = hcode & mask where mask = capacity - 1. Bitwise AND executes in 1 clock cycle; avoids multi-cycle integer division. hashtable.h:14-18
hashtable.cpp:7-13
2. Dual-Table Model Structured HMap with newer and older Directs queries to both tables while migration is active. Allows the server to remain fully operational while migrating keys in the background. hashtable.h:22-27
3. Rehash Trigger Checked load_factor >= 8 on insert Promotes newer to older; allocates doubled newer table. Prevents excessive chain lengths that would degrade lookup performance to $O(N)$. hashtable.cpp:48, 72-78
4. Incremental Step Migrated $\le 128$ slots per operation Moves linked nodes from older to newer on every user query. Bounds execution time per request, completely eliminating stop-the-world latency spikes. hashtable.cpp:49, 52-70
5. Pointer-to-Pointer Returned HNode ** in h_lookup Dereferences incoming address to splice node out: *from = node->next. Achieves branchless, symmetric deletion whether node is bucket head or chained interior. hashtable.cpp:32-46

โ“ Anticipated Interview Questions & Crisp Answers

Q1: Why implement custom progressive rehashing instead of using std::unordered_map?

Answer: std::unordered_map performs a synchronous "stop-the-world" rehash when its load factor threshold is breached. For a database holding 5 million keys, allocating a doubled bucket array and re-inserting all 5 million items freezes the event loop for 50โ€“150 milliseconds, violating low-latency SLOs and causing TCP client timeouts. Our HMap splits the work across client requests, migrating at most 128 non-empty buckets per query (hm_help_rehashing), bounding each step to sub-microsecond latency and achieving smooth $O(1)$ amortized cost.

Q2: What edge cases and consistency bugs occur when querying, inserting, or deleting keys during active rehashing?

Answer: During migration, keys exist across both newer and older. If queries searched only one table, keys would appear missing. hm_lookup searches newer first; if absent, it checks older. For insertions, new keys are always inserted into newer, ensuring older monotonically drains and never grows. For deletions, hm_delete searches both tables and uses double-pointer unlinking (h_detach). Most importantly, every operation runs a bounded hm_help_rehashing step before searching, guaranteeing forward progress on migration even under heavy read loads.

Q3: Why does h_lookup return a double-pointer (HNode **) and how does it prevent pointer-chasing bugs?

Answer: In separate chaining with singly-linked lists, unlinking a node requires modifying the pointer that addresses it. That pointer is either a slot entry in the bucket array (&htab->tab[pos]) or the next member of a predecessor node (&prev->next). Returning HNode **from provides the exact memory location of that incoming pointer. Detaching the node is a branchless, single instruction: *from = (*from)->next. This eliminates conditional branches for head-of-bucket vs interior nodes, preventing dangling pointer corruptions and redundant traversals.