A store places every value in the smallest of a set of fixed-size blocks — where does the wasted memory come from?
answer
- a few sizes, not any size
- smallest block that fits
- the remainder inside is unusable
- the boundary decides, not the value
- present before any deletion happens
basics
~20 sA store allocating from fixed-size blocks puts each value in the smallest block that fits and wastes the remainder of that block. The waste is set by which boundary the value just crossed, not by how big the value is.
solid answer
~50 sSome stores do not ask for exactly as many bytes as a value needs. They carve memory into **fixed-size allocation classes** — blocks of a few predetermined sizes — and place each value in the smallest block it fits. Placement becomes a lookup, not a search: no hunting for a hole, no splitting, no coalescing afterwards, and a predictable cost every time. The price is **per-block waste**: whatever is left over inside the block is unusable by anything else. That waste tracks the distance from the value to the top of its block, so values sitting just past a boundary are the worst case — where the step between classes is large, the waste can approach the payload again. It is there from the first write, before anything is deleted. Other stores instead hand values to a general-purpose allocator with finer steps of its own.
go deeper
Remember that a store may round each value up into one of a few block sizes, so the memory a value occupies is usually more than the number of bytes you handed over.
Explain that the waste is the gap between the value and the top of its block, so it depends on which boundary the value just passed. Say that it exists from the first write, with nothing deleted.
Compare the size distribution of a real workload against the boundaries and show what a few bytes of payload growth would do to the total. Know that not every store in this class works this way.
Weigh the strategy itself: constant-time placement and predictable reclaim bought with per-block waste, and decide whether shaping payloads under a boundary is worth the fragility it introduces.
## Blocks of predetermined sizes Some in-memory stores never ask a general-purpose allocator for exactly as many bytes as a value needs. They carve their memory into blocks of a few predetermined sizes — a set of **fixed-size allocation classes** — and place each value into the smallest block it fits in. That turns placement into a lookup. Pick the class, take a free block from it, write the value, done. There is no search for a hole of the right size, no splitting a large hole into a used part and a remainder, and no coalescing of neighbouring free space afterwards. The time to place a value does not depend on what the memory currently looks like, which matters a great deal for a store whose entire promise is an operation measured in microseconds. ## Where the waste is Every value that does not exactly fill its block leaves the rest of that block unused. Nothing else can be put there: the block belongs to this value until the value is gone. The important property is that **the waste is not a fraction of the value**. It is the distance from the value to the top of the block it landed in. Two values a few bytes apart in size can waste wildly different amounts, because one of them is just under a boundary and the other is just over it. Suppose a store's classes were 64, 128 and 256 bytes. These numbers are invented for the illustration and are not any store's: | payload | block it lands in | wasted inside the block | |---|---|---| | 60 bytes | 64 | 4 bytes | | 66 bytes | 128 | 62 bytes | | 130 bytes | 256 | 126 bytes | Six bytes of extra payload between the first row and the second costs about sixty bytes of memory. Nothing about the payload itself tells you which row you are on; only the boundaries do, and they belong to the store. ## The workload that makes it worst A workload whose values cluster just past a boundary is the bad case, and it is common, because value sizes are rarely random — they are produced by one serializer, one schema, one set of field names, so they cluster. - Where the step between classes is large, a value one byte past a boundary wastes close to what it holds: the payload again, give or take. - Where the step is small, the same workload wastes a modest percentage. - The best case is a workload whose values sit just under a boundary, which wastes almost nothing — and which one added field can destroy for every value at once. How large the step is differs by store. Classes usually grow by a ratio rather than by a constant, so the absolute step gets bigger as values get bigger, and some stores let an operator change that ratio. Making the step smaller means more classes: a tighter fit per value, but more partly-filled blocks spread across more classes. ## It is there from the first write This is a placement cost, not the residue of a long-running process. Load a freshly started store once, delete nothing, and the per-block waste is already present and already measurable. That distinguishes it from memory a running process holds that belongs to no entry — a different subject with a different diagnosis. If memory exceeds your payload arithmetic on a store that has done nothing but accept one bulk load, per-block waste is a sufficient explanation on its own. ## Why a store accepts the waste 1. **Placement is constant-time.** No search, so the worst case equals the typical case. 2. **Reclaim is trivial.** A freed block is immediately reusable by the next value of that class, with nothing to merge. 3. **Behaviour is predictable under load.** An allocator that searches can degrade as memory gets busy; one that indexes into classes does not. 4. **The cost is bounded.** The worst per-value waste is capped by the step between adjacent classes, which you can reason about in advance. ## Where stores differ - Some stores use fixed-size classes; others hand each value to a general-purpose allocator, which rounds requests up to steps of its own — usually much finer ones, so the same effect exists but is far smaller. - The number of classes and the step between them differ between stores, and are sometimes an operator's choice. - A store that keeps a compact and a general representation of structured values is attacking the same footprint problem from a different direction, on a different axis. So "an in-memory store rounds each value up into a block and wastes the rest" is true of some designs in this class and not of others. Say which you mean before you use it in a calculation. ## What to do with this - Compare the **distribution** of your value sizes to the boundaries, not the mean of them; a mean sitting comfortably below a boundary tells you nothing about the tail sitting above it. - Treat "the value grew by a few bytes and the memory jumped" as a boundary crossing until something else is proven. - If you can shape the payload — shorter field names, a tighter encoding, one fewer optional field — remember the goal is not to be small. The goal is to be under the next boundary, and a byte is enough. - Measure rather than compute. Payload arithmetic cannot see a boundary it was never told about.
- A value grew by a few bytes and the memory it holds nearly doubled — what happened?It crossed a block boundary. The store does not grow a block in place; it places the value in the next class up, and the whole of the new block is committed to it. Where classes grow by a large ratio, crossing one boundary is close to doubling, which is why a tiny payload change can show as a large memory change.
- Why choose fixed-size blocks over a general-purpose allocator at all?Speed and predictability. Placement is an index into a class rather than a search, reclaim needs no merging of free space, and the cost does not drift as memory gets busy. For a store selling microsecond operations that determinism is worth paying for, and the per-block waste is the bill.
- How would you cut per-block waste for values that cluster just past a boundary?Two levers. Shape the payload so it lands under the boundary — shorter field names, a tighter encoding, one dropped optional field — which is a code change with a measurable target. Or, where the store allows it, narrow the step between classes, accepting more classes and more partly-filled blocks in exchange for a tighter fit.
A shipping counter stocks three box sizes. A mug a finger wider than the small box ships in the medium one, and the rest of that box is packing air. The counter is fast precisely because nobody measures anything — it only asks which box the item fits in — and the air is what that speed costs. Two items a finger apart can ship in boxes twice apart in size.
saying these in an interview costs you the question
- Assumes the memory a value costs equals its byte length.
- Thinks per-block waste only appears after entries have been deleted.
- Believes every in-memory store allocates from fixed-size blocks.
- Treats the waste as a leak to be fixed rather than a placement cost.
- Expects slightly larger values to always cost slightly more memory.