skip to content

Why can separate chaining's per-entry overhead exceed the payload for a table of tiny values?

level: seniorimportance: nice to knowfreq 30%

answer

  1. Count the bytes, not the operations
  2. What does each entry carry besides its data
  3. One allocation per entry has a price
  4. Pointers and headers do not shrink with payloads
  5. The bucket array is overhead too

basics

~20 s

Every entry becomes its own allocated node carrying a next pointer, a cached hash, an allocator header and padding. That is tens of bytes of structure around a four-byte reading, plus a mostly-empty bucket pointer array on top.

solid answer

~50 s

Chaining pays a fixed structural tax per entry that is independent of how small the entry is. Each node holds the key, the value, a next pointer (8 bytes on a 64-bit machine), usually a cached hash value, and is rounded up to an allocation granule with its own allocator header — so a table of four-byte sensor readings can spend 30-40 bytes per reading on structure. On top of that sits the bucket array itself, one pointer per bucket, mostly null at a low load factor. Storing entries inline in a single array avoids the per-node pointer and the per-allocation header entirely, which is the memory argument against chaining for small fixed-size payloads; it pays for that with a lower usable load factor and a more delicate deletion story. A middle path keeps chaining but packs several entries per node so one pointer amortizes over many payloads.

go deeper

for a junior

Know that each entry in a chained table is its own small allocated node holding a next pointer alongside the key and value, so a table always costs more memory than the data it stores.

for a middle

Break the per-entry cost into parts you can name: next pointer, cached hash, allocation header, alignment rounding, plus a share of the bucket pointer array that grows as load factor drops.

for a senior

Size the structure against a real budget: quantify bytes per entry for a small payload, compare against inline storage, and say what the inline layout costs you in usable load factor and deletion complexity.

for a principal

Own the call across a fleet — weigh the memory saved against migration risk and the cost of a layout your team must maintain, and decide when a packed-bucket compromise beats changing strategy outright.

## The accounting nobody does Engineers reason about hash tables in operations per second and almost never in bytes per entry. For a table holding a few thousand rich objects that is fine — the structural overhead disappears next to the payload. For a table holding tens of millions of *tiny* values it inverts, and the table's footprint is dominated by the machinery rather than the data. Take a fleet of services each holding a table of sensor identifiers mapped to a four-byte reading. Count what one entry actually costs under classic chaining on a 64-bit machine: | Component | Typical bytes | |---|---| | Key | 8 (a fixed-width identifier) | | Value (the reading) | 4 | | `next` pointer | 8 | | Cached hash value | 4-8 | | Object/allocation header | 8-16 | | Alignment and size-class rounding | 0-8 | | **Node total** | **~32-48** | | Share of the bucket pointer array | 8 / load factor | The payload is four bytes. The entry costs roughly forty. **Ten times the data is structure**, and at fleet scale that is the difference between a service that fits its memory ceiling and one that does not. ## Where each byte comes from **The next pointer** is chaining's defining cost. It exists so a bucket can hold more than one entry, and it is paid on every entry whether or not that bucket ever collides. **The allocation header and rounding** are the cost of each entry being a *separately allocated object*. General-purpose allocators keep per-allocation bookkeeping and round requests up to size classes, so a small node's real footprint is larger than the sum of its fields. Millions of small allocations also fragment the heap and cost allocation time on every insert. **The cached hash** is a deliberate time-for-space trade: it lets a walk skip expensive key comparisons and lets a resize rehash without recomputing hashes. Worth it for string keys, pure overhead for a fixed-width integer key. **The bucket array** is one pointer per bucket, and a *lower* load factor makes this term worse: at `alpha = 0.5` you carry two bucket pointers per stored entry, mostly null. ## The comparison that matters Storing entries **inline in one array**, as open addressing does, deletes two of those lines outright: there is no per-entry next pointer and no per-entry allocation header, because there are no per-entry allocations at all. A slot is just key plus value plus a small amount of state. The same four-byte reading might cost 16 bytes instead of 40. That saving is not free. Inline storage must keep spare slots to stay fast, so a lower usable load factor gives back some of what was saved, and deletion becomes more delicate because a slot cannot simply be cleared without breaking later lookups. It also changes what the memory *looks like*: a chained walk follows pointers into memory the previous node did not bring with it, so each hop is a dependent access, whereas an inline scan touches consecutive addresses. For a small fixed-size payload the inline layout usually wins on both bytes and access cost; for large or variable-size values, or where simple deletion matters, chaining's per-node cost buys real properties. This is a genuine fork in the road, not a settled question — mainstream runtimes disagree about it. Java's and C++'s standard hash maps are chaining-based (the C++ container's bucket interface effectively mandates it), while Python's and Rust's are open-addressed. Same concept, different bets about which cost matters more. ## The middle path Chaining is not obliged to allocate one node per entry. A bucket can hold a small **array of entries** — several key/value pairs packed together with a single overflow pointer for the rare bucket that exceeds them. One pointer and one allocation header then amortize over several payloads, and the packed entries are scanned consecutively rather than chased. This keeps chaining's easy deletion and gentle degradation while cutting the per-entry tax substantially, and it is why real high-performance chained tables rarely look like a textbook singly linked list. ## How to raise this in an interview The move that lands is to *quantify* rather than assert. "For four-byte values, chaining costs roughly forty bytes per entry — a next pointer, an allocation header, rounding, and a share of the bucket array — so about ninety percent of the table is structure. Inline storage would cut that to around sixteen at the cost of a lower usable load factor and a harder deletion path." That answer shows you can size a data structure against a memory budget, which is the judgment the question is really testing.

  • Does lowering the load factor reduce a chained table's memory footprint?
    No — it increases it. Lowering the load factor means more buckets for the same entry count, and each bucket is a pointer whether or not anything hangs from it. At `alpha = 0.5` you carry two bucket pointers per stored entry. Lowering `alpha` buys shorter chains and lower latency; it spends memory to do it, so it is the opposite lever from the one this problem needs.
  • How would you keep chaining but cut the per-entry overhead?
    Pack several entries per node instead of one. A bucket node holding a small array of key/value pairs, with a single overflow pointer, amortizes one pointer and one allocation header across several payloads, and the packed entries are scanned consecutively. You keep chaining's simple deletion and gentle degradation while removing most of the per-entry tax.
  • When is chaining's per-node overhead simply not worth worrying about?
    When the payload dwarfs it. For entries holding strings, nested objects or buffers of hundreds of bytes, forty bytes of node structure is noise, and chaining's simpler deletion and tolerance of high load factors are the properties that actually matter. The accounting only turns decisive for small fixed-size values at large entry counts.

saying these in an interview costs you the question

  • Counts only key and value bytes per entry
  • Forgets that each node is a separate allocation
  • Thinks lowering load factor saves memory
  • Ignores the bucket pointer array entirely
  • Assumes overhead scales with payload size

context