A tile renderer stores coordinate points in a generic container; why does each point cost an allocation and an extra memory hop?
answer
- one representation for every argument
- references are the common shape
- a number is not an address
- wrapper object plus its address
- two memory touches per element read
basics
~20 sA shared container body is compiled once against one uniform slot shape — a reference — so an unboxed point cannot sit in the buffer directly. Each point becomes a separate heap object and the slot holds that object's address.
solid answer
~50 sA generic container that is compiled once and reused for every element type has to choose a single machine-level shape for a slot, and the only shape every element can take is an **address**. A coordinate point is a small bundle of numbers, not an address, so putting it in the buffer means first giving it one: the runtime allocates a small object that holds the coordinates and writes that object's address into the slot. That is the allocation, and it also carries an object header and alignment padding, so the footprint per point is well above the payload. It is also why a read is two memory touches instead of one — read the slot to get the address, then follow it to the object holding the numbers. Sequential iteration loses the flat block's locality, because the targets are separate objects wherever the allocator put them.
code
pseudocode · 12 lines// shared body: every element slot has the same shape, a reference
container Buffer<T>:
slots = array of reference // one shape, chosen for every T at once
function get(i):
address = slots[i] // touch 1: read the slot
return address.value // touch 2: follow it to the object
// storing a point must first give the point an address
p = Point(x = 512, y = 384)
wrapper = allocate object holding p // one object per stored point
buf.slots[i] = wrapper // the slot keeps only the addressgo deeper
Recall that a generic container holds references, and that a plain number has to be wrapped in an object before it fits. That wrapper is a real allocation with a real size.
Explain why the slot shape is fixed: one body compiled for every element type can only assume the shape they share. Then walk the two memory touches and the per-element footprint out loud.
Show where this bites in production — a large long-lived buffer scanned repeatedly, where footprint and scattered targets cost more than the allocations themselves, and say how you would confirm that.
Frame it as a platform trade: what storage the platform gives you for element types, what the workload's hot buffers look like, and whether the cost justifies owning a non-generic storage path.
## The one representation a shared body can assume A container that is compiled **once** and reused for every element type has to commit, at the moment it is compiled, to a single machine-level shape for "an element". It cannot commit to a number's shape, because element types differ in width and in how they are copied. The shape every heap-allocated value already has in common is **an address**, so the shared body is compiled against a reference-sized slot. Everything the body does with an element — store it, move it during growth, hand it back to a caller — is written in terms of that slot. A coordinate point in a tile renderer is a pair of numbers with no identity of its own: conceptually a handful of payload bytes. It is not an address. To go into a reference-shaped slot it must first be turned into something that has an address — a small heap object holding the numbers, whose address is written into the slot. That conversion is **boxing**, and the object is the **wrapper**. ## What each point actually costs - **An allocation.** A wrapper is an object, so filling a buffer of `n` points allocates `n` objects rather than one block — for every point not served from a cache of pre-made wrappers. - **A header.** Every heap object carries runtime bookkeeping ahead of its fields. For an element of a few bytes, that header is frequently as large as the payload or larger. - **Padding.** Objects are aligned, so the real size of a wrapper rounds up. - **The slot itself.** The buffer still spends a full reference-sized slot per element, on top of the wrapper it points at. - **Reclamation work.** Every wrapper is a separate object the memory reclaimer must trace and eventually free, where a flat block is one object however many elements it holds. ## Why every read is two memory touches In a flat block, element `i` lives at a computed offset: one touch, and its neighbours arrive in the same cache line, so a hardware prefetcher can run ahead of a sequential scan. In a reference buffer, the slots are contiguous but the **targets are not**. Reading point `i` touches the slot to obtain the address, then touches whatever the allocator chose for that wrapper. A scan of `n` points therefore walks `n` scattered objects. The addresses stream nicely; the coordinates do not. ## A flat block against a buffer of references | | flat block of points | buffer of references to wrapped points | |---|---|---| | bytes per element | the payload, nothing else | slot + header + payload + padding | | memory touches per read | one | two: the slot, then the wrapper | | allocations to fill it | one, for the whole block | one per point not served from a wrapper cache | | sequential locality | neighbours share cache lines | depends entirely on where wrappers landed | | reclaimer's view | a single object to trace | every wrapper is its own object | ## The honest hedges The model above is the shape of the cost, not a guarantee about any particular run: 1. Some runtimes keep a cache of pre-made wrappers for a small range of values, so wrapping is not unconditionally an allocation. Coordinates rarely fall inside such a range. 2. An optimizer that can prove a wrapper never escapes may keep the value in registers and allocate nothing — which applies to short-lived temporaries, not to a value stored in a buffer that outlives the call, because storing it is exactly what makes it escape. 3. Wrappers allocated back to back may land adjacent, so a freshly filled buffer can scan almost as well as a flat one. The gap opens as the heap ages and points are replaced individually. ## What this is not about This is not a consequence of discarding type arguments. A platform that keeps the argument available at run time still runs **one shared body** unless a specialized one is generated, and the slot shape was fixed when that shared body was compiled. Knowing what the element type is does not, on its own, change where the element is stored. The change comes only from a body whose storage is laid out for this exact element — which is what removes the wrapper, the header and the second memory touch together.
- Does a platform that keeps type arguments available at run time avoid the wrapping?Not by itself. Keeping the argument available tells the body what the element type is; it does not change the slot's shape, which was fixed when the shared body was compiled. The wrapping disappears only when a body is generated whose storage is laid out for that exact element width.
- Why is a wrapper often larger than the numbers it holds?Every heap object carries a header of runtime bookkeeping ahead of its fields, and objects are aligned, so the real size rounds up. The buffer then still spends a full reference-sized slot on the address. For an element of a few bytes the overhead can be several times the payload.
- Does wrapping always cost an allocation?No. Some runtimes hand out pre-made wrappers for a small range of values, and an optimizer that proves a wrapper never escapes can keep the value in registers. Neither helps here: coordinates rarely fall in a cached range, and storing a value in a long-lived buffer is what makes it escape.
A flat block of points is a printed table of coordinates: the next row is the next line on the page. A buffer of wrappers is a table of page numbers — you read a number, then go and find that page.
saying these in an interview costs you the question
- Thinks the generic buffer stores the numbers directly and only the type label is dropped
- Says a wrapper costs only the bytes of the values it holds
- Assumes scanning wrapped points is as cache-friendly as a flat block
- Believes an unboxed value can occupy a reference slot without conversion
- Treats the cost as allocation alone, ignoring the second memory touch per read