skip to content

Compact Representations

Small entries pay a fixed overhead, and stores use different schemes to cut it. Each scheme has a size past which the saving stops, which is where memory jumps without warning.

on this pageshow

questions

4

An in-memory store's memory steps sharply when its small maps each gain their hundredth field — which two representations explain that step?

level: middleimportance: must knowfreq 58%

answer

  1. one value, two internal layouts
  2. packed while small, structured once big
  3. several axes, each with a threshold
  4. the whole value is laid out again
  5. memory steps, it does not climb

basics

~20 s

Many in-memory stores keep two representations of one value: a compact form that packs elements together, and a general form with a lookup structure per element. Crossing the promotion threshold swaps them, so memory steps rather than climbs.

solid answer

~50 s

A small collection need not be stored the way a large one is. Stores that do this keep a **compact representation** — elements packed into one contiguous run of bytes, with no per-element structure — and a **general representation** giving every element its own place in a lookup structure. While the value stays small the store uses the compact form; once it crosses a **promotion threshold** the store lays the value out again in the general one. The threshold sits on more than one axis — element count, the size of a single element, sometimes the element type — and the swap is invisible to application code. Because the whole value is rebuilt at once, memory jumps in a step rather than climbing. Not every store does this: some apply it to several value shapes, some to one, some hold opaque bytes and have no second form.

go deeper

for a junior

Remember that a store can hold the same small collection in more than one internal layout, and that the layout, not only your data, decides what it costs in memory.

for a middle

Describe both layouts and name the axes that trigger the swap: element count, the size of one element, sometimes the element type. Explain why the change arrives as a step rather than a slope.

for a senior

Show that you would locate the step by measuring across a range of sizes rather than by quoting a threshold, and that you would ask what grew in the value before suspecting the allocator.

for a principal

Take a position on the trade: shaping values to stay under a threshold buys memory and costs fragility, because one added field can move a whole class of entries across at once.

## The same value, two internal layouts Many in-memory stores hold a value the caller thinks of as one thing — a small map of fields, a short list, a small set of numbers — in one of two internal layouts, and choose between them without telling anyone. The **compact representation** puts the elements into one contiguous run of bytes behind a small header, with just enough length information to walk from one to the next. There is no slot, no pointer and no separately allocated node per element: the elements are the storage. The **general representation** gives every element its own place in a lookup structure, so the store reaches one element without walking the rest. That costs a slot per element, the structure's own bookkeeping, and usually a separate allocation each. When the elements are small, this per-element machinery is not a rounding error: on some stores and some value shapes it costs more than the elements themselves. That is why the compact layout exists — for a value with a handful of short elements, packing removes most of what it would otherwise cost. ## What moves a value across the line The store abandons the compact layout when the value gets big enough that walking it stops being cheap. "Big enough" is not one number, and it is not one axis: - **element count** — the value gains its n-th element; - **the size of one element** — a single field grows from a short string to a long one, and one long element is enough on its own, however few there are; - **sometimes the element type** — some stores pack only elements of a particular kind, for instance small whole numbers, and switch the moment anything else is added. Each axis has its own threshold, each is a value the store chooses and often lets an operator move, and they differ between stores and between value shapes. What transfers is that the thresholds exist and which axes they sit on; the numbers do not. ## Why memory steps instead of climbing The crossing is not incremental. The store rebuilds the whole value in the other layout in one move, so the memory attributed to that entry changes at once. | | compact representation | general representation | |---|---|---| | stored per element | the bytes, plus length information | the bytes, plus a slot, bookkeeping and usually its own allocation | | finding one element | walk from the start | reach it directly | | changing one element | may rewrite part of the run | touches that element | | memory as elements are added | roughly the bytes added | the bytes added plus a fixed cost each time | Add one element to a value sitting just under a threshold, across a million such entries, and the memory they occupy multiplies. How large the multiple is depends on the store and on how small the elements were — the smaller they are, the bigger the jump, because the fixed per-element cost dominates. Nothing in the workload looks different: the same call, one more field. Monitoring shows a step, not a slope, and an alert tuned for gradual growth does not see it coming. ## Nothing in application code changes The layout is a private decision of the store: the same operations, the same results, and no error or type change to mark the crossing. That is the point of the scheme and also its hazard — there is no line in the application where anyone could have noticed. In most stores that do this, the crossing is also effectively one-way. Checking on every removal whether the value could go back to the packed layout would put work on the hot path for a rare benefit, and values hovering at the boundary would rebuild repeatedly, so the value keeps the general layout until it is written again from scratch. ## The trade, in both directions The compact layout is scanned, not indexed: reaching one element means walking from the start, so the cost of touching an element grows with the element count. For a handful that walk is short, and the contiguity can make it as fast in practice as a lookup; for thousands it is not close. The threshold sits near where the footprint saving stops paying for the access cost. Neither layout is simply better — the compact one is a win only while the value is small. ## Where stores differ - Some apply the scheme across several value shapes, aggressively. - Some apply it to one shape only. - Some hold values as opaque bytes the server never interprets, so there is nothing to pack and no second representation at all; those stores attack footprint elsewhere, for instance with fixed-size blocks. - Where the scheme exists, the axes and the thresholds are the store's own. So "small collections are packed and promoted past a threshold" is a true statement about a family of stores, not about this class of store. Say which behaviour you are assuming before you plan around it. ## What to do with this 1. Do not extrapolate a footprint from one measured size; measure across the range of sizes the workload will really produce. 2. Expect the number to move in steps, at boundaries you did not choose. 3. When memory jumps after a harmless-looking change, ask what grew — an element count, one element's size, or the type of what was added — before reaching for the allocator.

  • If the compact layout uses less memory, why does the store ever abandon it?
    Because it is scanned rather than indexed. Reaching one element means walking from the start, so the cost of every read and every change grows with the element count. While the value is small that walk is short enough to be competitive; past some size it is not, and the store trades footprint for direct access.
  • Will two different stores report the same memory for the same data?
    No, and not by a small margin. Whether a compact representation exists at all, which value shapes it covers, which axes trigger the swap and where the thresholds sit are all the store's own choices. A footprint measured on one store is evidence about that store, not about the data.
  • How would you find the step before production finds it for you?
    Empirically. Take a representative entry, write it at a range of sizes spanning what the workload will produce, and record what the store attributes to it at each size. The jump shows up as a discontinuity you can bisect toward. Repeat per value shape, since the axes differ between shapes.

saying these in an interview costs you the question

  • Assumes memory always grows in proportion to the elements added.
  • Thinks the compact layout is used at every size because it is smaller.
  • Says the application sees an error or a type change at the crossing.
  • Quotes one store's threshold as the number every store uses.
  • Believes every in-memory store keeps two representations of a value.
  • Blames the allocator for a jump that followed a change in value shape.
open as a page

A store places every value in the smallest of a set of fixed-size blocks — where does the wasted memory come from?

level: middleimportance: should knowfreq 44%

basics

~20 s

A 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.

open as a page

A bulk load briefly made every collection in a store large; the collections were trimmed back, yet the store's own data size stayed high — why?

level: seniorimportance: should knowfreq 46%

basics

~20 s

Stores that swap to a general representation at the threshold usually never swap back when the value shrinks: re-checking on every removal costs work and would thrash at the boundary. The value keeps the larger layout until it is written again.

open as a page

You must size a store for entries whose sizes sit near both a promotion threshold and a block boundary — how do you plan?

level: principalimportance: should knowfreq 34%

basics

~20 s

Measure the real size distribution against both steps rather than extrapolating one measurement. A promotion threshold and a block boundary each turn a small payload change into a large memory jump, so say which side of each the workload sits on.

open as a page