When joining thousands of text fragments into one document, why compute the total length first?
answer
- both approaches are linear — so what differs?
- count allocations, not just character copies
- what is live during a reallocation?
- the growth step lands inside some request
- the measure pass needs re-traversable fragments
basics
~20 sSumming the fragment lengths first lets you allocate the result exactly once and fill it in one pass: no reallocation copies, no slack, no growth pause. A growable buffer ties asymptotically but loses on peak memory and tail latency.
solid answer
~50 sBoth approaches are linear in the output size, so this is not a complexity decision — it is an allocation decision. A growable buffer performs a logarithmic number of reallocations, each copying everything buffered so far, holds both old and new blocks live during a copy, keeps slack capacity afterwards, and typically needs one more copy to produce the final immutable result. The two-pass alternative walks the fragments once to sum their lengths, allocates exactly that many characters, and walks them again to fill: one allocation, each character written once, no slack, and no growth step that could land inside a request's latency budget. The cost is that fragments must be re-traversable — a one-shot stream rules it out — and that lengths must be cheap to obtain. Default to the buffer; measure first when the document is large and you are defending a memory ceiling or a tail-latency target.
go deeper
Know that both ways of assembling the document are linear in its length, and that computing the total first lets you allocate the exact size once instead of repeatedly enlarging a buffer.
Explain the mechanics of each: how many allocations happen, which characters get copied more than once, and what the final conversion to an immutable result costs. Be able to say why the asymptotic bounds are identical.
Argue the decision from production evidence — peak memory while two blocks are live, tail-latency spikes from a growth step inside a request, allocation churn under concurrency — and name the conditions that make the measure pass unusable.
Own the default for the codebase and the exception rule. Weigh the extra offset arithmetic and its bug surface against a memory ceiling across a fleet, and consider presizing from an estimate as the middle option that keeps the simple code.
## The scenario A service assembles a personalised message document from a few thousand fragments — template chunks, substituted values, repeated sections. The result is a few hundred kilobytes. Thousands of these are being assembled concurrently. Two strategies are on the table. **Strategy A — growable buffer.** Create a buffer, append each fragment, convert to the final immutable document at the end. **Strategy B — measure then fill.** Walk the fragment list summing lengths, allocate exactly that many characters, walk again copying each fragment into its slot, wrap the block as the result. ## Asymptotically they tie Both are O(m) in the output length m. Strategy A's reallocations copy a geometric series of characters, which sums to a constant multiple of m; Strategy B copies each character exactly once. Anyone who claims one is asymptotically better than the other has misread the analysis. **The decision lives entirely in the constants and in the allocation behaviour**, which is precisely why it is a senior question rather than a complexity question. ## What Strategy B actually buys **One allocation instead of many.** A growable buffer starting small and growing by a constant factor to reach m performs a logarithmic number of allocations. Each is a call into the allocator, and each abandons a block. Strategy B makes one request of the right size. **No re-copying.** Strategy A moves earlier characters once per growth step. Even though the total is bounded by a constant multiple of m, that constant is real work — you are copying a meaningful fraction of the document more than once. **Lower peak memory.** During a growth step both the old block and the new, larger block are live simultaneously. Add the final conversion into an immutable result, during which the buffer and the result are both live, and the transient peak can approach a small multiple of the finished document. Strategy B's peak is essentially the document itself. Multiply by the number of concurrent assemblies and this is the argument that actually persuades: under a per-instance memory ceiling, peak matters more than average. **No growth pause.** The reallocation-and-copy step is a single linear operation that lands inside whichever request happens to trigger it. That is a tail-latency event. Removing it removes a source of p99 variance — the same reasoning as presizing a buffer, taken to its logical end where the size is not estimated but computed. **Contiguity from the start.** The fill pass writes forward through one block that never moves, which is the friendliest possible access pattern. ## What Strategy B costs **A second traversal.** You walk the fragment list twice. The measure pass touches lengths, not characters, so it is cheap — but it is not free, and on small documents it is pure overhead against a buffer that would have grown twice and finished. **Fragments must be re-traversable.** If fragments arrive from a one-shot source that cannot be replayed — a generator, a stream, a cursor you cannot rewind — the measure pass consumes what the fill pass needed. Either materialise them first (which costs the memory you were trying to save) or use the buffer. **Lengths must be cheap.** Summing assumes each fragment's length is available in constant time. If obtaining a length requires scanning the fragment, the measure pass costs as much as the fill pass and the advantage narrows to allocation behaviour alone. **More code to get wrong.** An off-by-one in the offset arithmetic of the fill pass produces a corrupted document; the buffer version cannot make that mistake. If the document is small, the simpler code is the better engineering answer. ## The decision, stated as a rule Default to the growable buffer: it is simpler, it is asymptotically equivalent, and for most documents the difference is unmeasurable. Switch to measure-then-fill when **all** of the following hold: the output is large enough for its peak memory to matter; the fragments are already materialised with constant-time lengths; and you are defending something specific — a per-instance memory ceiling, a latency percentile, or an allocation-rate target — that you can point at. And note the middle option, which is often the right one: keep the buffer but **presize** it from an estimate. You get one allocation in the common case and the buffer's safety net when the estimate is short. That is frequently the best engineering trade, and offering it unprompted is what distinguishes a considered answer from a memorised one. ## The failure to avoid The weak answer is "a buffer is always right, measuring is premature optimisation". That is a reasonable *default* being mistaken for a *law*. The strong answer names what changes the decision — peak memory under concurrency, tail latency, allocation churn — and states what evidence would make you switch.
- Is summing the fragment lengths always cheap?Only when each length is available in constant time, which holds when fragments are already materialised with a stored length. If obtaining a length requires scanning the fragment — counting units of variable-width text, or pulling it from a lazy source — the measure pass costs about as much as the fill pass, and the remaining advantage is limited to allocation behaviour rather than total work.
- In practice, how do you decide between the two on a real service?Default to the growable buffer for simplicity. Switch to measure-then-fill when the document is large, the fragments are materialised with cheap lengths, and you are defending a named target: a per-instance memory ceiling under concurrency, a latency percentile, or an allocation-rate budget. Often the best middle ground is presizing the buffer from an estimate — one allocation in the common case, with the buffer's growth as the safety net.
Packing a shipment: you can keep upgrading to a bigger box as things pile up, or measure everything once and order the right box. Both ship; only one avoids repacking on the loading dock.
saying these in an interview costs you the question
- Claims measure-then-fill improves the asymptotic bound
- Says a growable buffer is always right and measuring is premature
- Counts only total copying and ignores peak memory
- Forgets both blocks are live during a reallocation
- Assumes fragments can always be traversed twice