A team made every computed field of a record lazy; memory and latency both rose though fewer fields are read. What are they paying for?
answer
- deferral is a bet, not a saving
- an object per deferred value
- every read tests before it reads
- the wrapper can outweigh the result
- paid per field, per record, per request
basics
~20 sEvery deferred field costs an object on the heap plus a check and a pointer hop on each read. Where the computation is cheaper than that overhead, deferral adds allocation, indirection and collector pressure while saving almost nothing.
solid answer
~40 sDeferral is a bet: pay a fixed cost per value now in order to avoid a variable cost later, and it pays only when the value is often not demanded or is genuinely expensive to produce. Wrapping a field that adds two numbers the record already holds loses that bet twice - the cell is typically larger than the result it will hold, and every read now tests whether the slot is still deferred and follows a pointer to reach the value. Multiply by every field of every record and you get more allocation, worse locality and a first-touch cost landing on whichever request demands a field first. The fix is not to abandon laziness but to defer selectively: the expensive, rarely read fields, not all of them.
go deeper
Remember that deferring is not free: something must hold the unrun computation, and that something is an object allocated like any other.
Put the two costs side by side - the computation you skipped against the allocation and per-read test you added - and say which kinds of field come out ahead.
Treat it as a workload question: measure how often each field is actually demanded and how expensive it is, then defer only where the skipped work dominates the wrapper cost.
You own the default. Lazy everywhere buys simple code and an unpredictable tail; eager everywhere is predictable and wasteful on cold paths, and the defensible answer is a small deferred set justified by measurement.
## Deferral is a bet, not a saving It helps to state the arithmetic plainly. Deferring a value **saves** you the body's cost multiplied by the probability that nobody ever demands it. It **costs** you an allocation for the cell, a test and an indirection on every read, and the relocation of the work to whichever caller demands it first. Deferral wins when the first term dominates: an expensive body that most code paths never touch. It loses when the body is cheap, or when nearly every path demands the value anyway, because then the saved term is close to zero and the cost term is paid in full. Applying it to *every* computed field guarantees that the losing cases are included along with the winning ones, which is exactly what the team observed: memory up because there are now many small cells, latency up because reads got more expensive and the skipped work was never the bottleneck. ## What one deferred field costs - **An object per value.** A cell holds the unrun body, whatever that body needs, the result slot and the mark. For a field whose result is a single number, the wrapper is comfortably larger than the thing it wraps. - **A read path with a branch in it.** Reading the field means testing the mark and, if unset, running the body; if set, following a pointer to the stored value. A direct field read has neither step. - **Worse locality.** A record of computed values becomes a record of references to separately allocated cells. Reading several fields now touches several places instead of one contiguous block. - **Collector pressure.** Many small, short-lived cells are allocated and abandoned per record, and that cost scales with request volume rather than with how much work was skipped. - **Retention until the demand.** Each unforced cell keeps alive everything its body still needs, so inputs that would otherwise have been released at construction stay reachable. ## Which fields are worth it | Field | Body cost | How often it is read | Verdict | |---|---|---|---| | A sum of two numbers already in the record | trivial | any | compute it eagerly; the cell costs more than the work | | A large derived structure | high | rarely | defer; this is the case laziness exists for | | An expensive value nearly every caller reads | high | almost always | compute it once at construction and store the result | | A moderate computation read on some paths | moderate | mixed | measure before choosing; this is where intuition is worst | ## Why latency rose and not only memory The work did not disappear; it moved. Whatever the record's creator would have spent is now spent by the first caller to touch the field, so the cost lands on an arbitrary request rather than a predictable one. Averages can improve while the tail gets worse, and the tail is what users feel. On top of that, every read of every field now pays the test and the hop, including the many reads of fields whose values were already forced - a small cost, but one paid on the hottest path in the system rather than on the cold one deferral was meant to protect. ## What to do instead 1. **Measure per field**, not per record: how expensive is the body, and what fraction of requests demand it? Those two numbers decide each case on their own. 2. **Defer the short list** the measurements justify, and compute the rest eagerly. A handful of deferred fields captures nearly all of the available saving. 3. **Consider a third option** for values that are expensive and almost always read: compute once at construction and store the result. It has the sharing benefit without the per-read test. 4. **Keep the once-only guarantee** wherever deferral stays, so a shared record still costs one evaluation however many consumers read it. 5. **Re-measure after the change.** The failure mode here is a plausible model applied without numbers, and the correction is numbers rather than a different plausible model.
- Which fields here are genuinely worth deferring?The ones whose body is expensive relative to an allocation and which most code paths never read. The payoff is the cost you skip multiplied by how often you skip it, so a field read by nearly every caller should simply be computed, and a cheap field should be computed whatever its read rate.
- Why does deferral often make latency less predictable rather than lower?Because the work does not disappear, it relocates to whichever caller first demands the value. Total work may fall, but the cost now lands on an arbitrary request instead of on the one that created the record, so the average improves while the tail gets worse - and the tail is what users notice.
- Does forcing a value once remove the per-read overhead afterwards?Mostly, not entirely. After forcing, a read no longer runs anything, but it still reaches the value through the cell, and the test that asks whether the slot is still deferred is typically still performed. The evaluation cost is gone; the indirection generally stays.
saying these in an interview costs you the question
- Thinks deferring work always lowers total cost.
- Ignores the allocation each deferred computation needs.
- Assumes a forced value is read as cheaply as a plain field.
- Says laziness cannot raise memory use because it delays work.
- Treats first-touch latency as free because the work would have happened anyway.