skip to content

Two engineers blame different per-record costs for a slow two-million-row loop, so how do you establish which one dominates?

level: seniorimportance: should knowfreq 44%

answer

  1. order the costs, do not list them
  2. three input sizes before any profiler
  3. linear or superlinear answers it first
  4. ablate one component at a time
  5. only one cost's share grows with n

basics

~20 s

Measure at three input sizes first: linear growth means the per-record constants, superlinear means the accumulation is recopying and outranks everything. Then ablate, by iterating without working, working without accumulating and narrowing the table, and read the differences.

solid answer

~40 s

Do not argue the ranking, measure the curve. Run the same loop at three sizes and look at the shape: roughly linear means every cost in play is a constant per record, and roughly quadratic means something is redoing work already done, which is nearly always an accumulation that rebuilds the whole result each step. That term's share grows with the input, so it outranks the constants at production size regardless of how they compare. Once the curve is linear, ablate: iterate the per-record surface with an empty body to price assembly and wrapping, discard the result instead of accumulating to price the accumulation, and narrow the table to price the width-driven part. Four timings and three subtractions give a measured ranking rather than a recited one, and the ranking differs by ecosystem anyway.

go deeper

for a junior

Learn the habit before the technique: time the same loop at two input sizes rather than arguing about which part is slow. The shape of the change tells you more than any intuition will.

for a middle

Explain the ablation. Run the loop with the body emptied, then with the result discarded, then on a narrowed table, and attribute each difference to assembly, accumulation or width.

for a senior

Lead with the scaling test, because a superlinear term reorders the whole list. Only once the curve is linear is it worth arguing which constant is largest, and then measure that too.

for a principal

Decide what the investigation is buying. If the loop is going to be rewritten over whole columns anyway, the decomposition is a diagnosis rather than a backlog of four separate pieces of work.

## Why this is measured rather than argued A loop over records pays several distinct overheads once per record: resolving at run time what the values are, wrapping each value as a language object, assembling a record that is not stored anywhere, and whatever accumulating the results costs. All four are real. Their *sizes* differ by an order of magnitude depending on how wide the table is, how many rows there are, and which ecosystem you are in - which is exactly how two competent engineers end up holding opposite opinions with genuine evidence for each. There is a method, and it takes about ten minutes. ## Step one: scale the input before profiling anything Run the same loop at three sizes - *n*, 2*n*, 4*n* - and read the shape rather than the numbers. - **Roughly linear** (double the input, double the time): every cost in play is a constant per record. Now it is worth asking which constant is largest. - **Roughly quadratic** (double the input, quadruple the time): something is redoing work already done, almost always an accumulation that rebuilds the whole result on each iteration. Stop here. That term's share grows with the input, so it will dominate at production size no matter how the constants rank, and it is usually a few lines to fix. This single measurement orders the entire list. A profiler run first will cheerfully attribute most of its samples to the per-record surface and tell you nothing about the fact that the curve is bending. ## Step two: ablate, one component at a time With a linear curve confirmed, take the same loop and remove one thing at a time, re-timing each variant: 1. **Iterate and do nothing.** Walk the per-record surface with an empty body. This prices the record assembly and the wrapping on their own. 2. **Work, but discard the result.** Do the arithmetic and throw the answer away instead of accumulating it. The gap against the full loop is the accumulation's share. 3. **Narrow the table.** Reduce it to the columns the body actually reads and repeat variant 1. The gap is the width-driven part of the assembly. 4. **Walk one column's values instead of records.** Nothing is assembled at all, so what is left is the per-value costs. | variant | what its time contains | |---|---| | the full loop | everything | | iterate, empty body | assembly plus wrapping | | work, discard the result | everything except the accumulation | | narrowed table, empty body | assembly of the used columns, plus wrapping | | walk one column | per-value costs only | Four timings, three subtractions, and the ranking is measured instead of recited. ## What the answer usually looks like, with its conditions attached - **When the curve is superlinear**, the accumulation outranks everything, and the other three are not worth discussing until it is gone. - **On a wide table**, the record assembly usually leads among the constants, because it gathers and wraps one value from every column whether the body reads it or not. - **On a narrow table**, the wrapping and the run-time type decision usually lead, because there is barely anything to assemble. - **Where the host language is compiled and stores values unwrapped**, the type decision and the wrapping shrink sharply, since the operation is resolved before the loop runs and no object is built per value. Assembly and accumulation then take over. A ranking learned in one ecosystem does not transfer to another, which is a second and independent reason to measure. ## Traps that make the measurement lie - **Measuring at a convenient size.** A thousand-row benchmark hides the only superlinear term and over-weights any fixed setup. Measure at a size you actually run at, and at two sizes at minimum. - **Measuring once.** The diagnostic value lives in the ratio between sizes; a single number cannot produce a ratio. - **Timing the first run.** Warm-up, lazily initialised machinery and page faults all land in the first pass. Discard it, or amortise across repeats. - **Ignoring peak memory.** An accumulation problem shows as a memory curve as well as a time curve, and sometimes the memory curve is the one that actually hurts. - **Changing two things between variants.** Each ablation must remove exactly one component, or the subtraction means nothing. ## What to do with the ranking once you have it If the plan is to rewrite the loop as one expression over whole columns, all four overheads go at once and the decomposition was a diagnosis rather than a work list. The ranking earns its keep in the other cases: when a partial fix is needed today, when only part of the body has a whole-column form, and when you have to explain to a colleague why the obvious-looking change they proposed will not move the number at all.

  • The timing curve is linear. What have you already ruled out?
    The recopying accumulation. A combination that rebuilds the whole result on each iteration grows with the square of the row count, so a clean doubling on doubled input says it is not happening: either the code collects and combines once, or the design records the operation and materialises at the end. What remains are constants per record - the type decision, the wrapping and the assembly.
  • Why measure the table's width as well as its length?
    Because the per-record assembly gathers one value from every column, so its share of the per-iteration cost tracks the width. On a two-column table the wrapping and the type decision dominate; on a forty-column table the assembly does, and narrowing first is then the single highest-value change available.
  • What does a compiled host language with unwrapped storage change about the ranking?
    It shrinks the two per-value costs, because the operation is resolved before the loop runs and no object is built per value. What is left is the per-record assembly and whatever the accumulation does. So the ordering is not a property of the pattern alone, it is a property of the pattern and the ecosystem together, which is why reciting a ranking travels badly.

saying these in an interview costs you the question

  • Asserts a ranking from memory without measuring anything at all.
  • Benchmarks once, at whatever size was convenient to run.
  • Profiles line by line before checking how the time scales.
  • Treats all the per-record costs as equally sized everywhere.
  • Forgets that the table's width drives the assembly's share.