skip to content

Whole Columns at a Time

Working an entire column at once instead of looping over records, and the stretching, callbacks, text surfaces and temporaries that come with it. Interviewers use it to tell habit from understanding.

on this pageshow

explore

questions

27

Two numeric columns of one million values are multiplied by one written expression - what does it hand back, and how many dispatches?

level: juniorimportance: must knowfreq 78%

answer

  1. one call, not a million
  2. a column comes back, not a number
  3. the loop moved, it did not vanish
  4. new buffer allocated, operands left alone
  5. in-place forms are the exception

basics

~20 s

It hands back a newly allocated column of one million products, one per position, and leaves both operands as they were. Your program makes one dispatch, but the million-step loop still runs - inside the library's compiled pass.

solid answer

~40 s

Saying the operation once for the whole column rather than once per row - working column-wise - turns a million trips through your own program into a single call. What comes back is a freshly allocated column of a million values, one result per position, and the plain operator form leaves both operands untouched. The loop did not disappear; it moved. Inside the library's compiled pass each step reads raw bytes of a known width, multiplies and writes raw bytes, instead of unwrapping a full language object, deciding its type, finding the right multiplication and wrapping the answer back up. One caveat on `untouched`: the in-place operator forms write into a buffer that already exists, so anything else pointing at that buffer sees the change.

code

pseudocode · 7 lines
pseudocode
# one dispatch per value: your program runs the body n times
result = new column of length n
for i in 0 .. n - 1:
    result[i] = price[i] * quantity[i]

# one dispatch in total: the same n steps run inside compiled code
result = price * quantity

go deeper

for a junior

Be able to say what comes back: a new column the same length as the operands, one result per position, with the originals unchanged. That plus "one call instead of a million" clears the screening bar.

for a middle

Explain what one step costs on each side - a boxed value unwrapped and type-checked per value, against raw bytes of a known width - and state plainly that the loop moved into compiled code rather than disappearing.

for a senior

Price the rewrite in memory as well as time: the result is a whole new buffer that is live alongside both operands. Know when the in-place form is the right tool and what else can see that write.

for a principal

The interesting tradeoff is what a standing rule costs a team. Mandating the column-wise form buys predictable speed and costs peak memory and readability in places; say which of those your workload can afford.

## What the expression hands back Saying an operation once for a whole column instead of once per row - working **column-wise** - produces **a new column**, one result per position, from a single call in your program. Three separate facts live in that sentence, and an interviewer is usually checking all three. - **A column, not a number.** An element-wise operator is defined per position: the value at offset *i* of the left operand combines with the value at offset *i* of the right, and the result at offset *i* is that combination. A million pairs in, a million results out. An operation that folds a column down to a single value is a different kind of operation with a different cost model entirely. - **A fresh allocation.** The plain operator form writes nowhere that already exists. It asks for a buffer the size of the result and fills it, so a million products over eight-byte values cost roughly eight megabytes of new memory, and both operands still hold exactly what they held. - **One dispatch from your program.** Your program executed one statement. What it did *not* do is decide, a million separate times, what kind of things it was multiplying and which multiplication applied. ## The loop did not vanish - it moved This is the part candidates most often get wrong, and it is worth being exact. There is still a loop, and it still runs a million times. What changed is **where** it runs and **what one step of it costs**. In a loop you write in your own program, each step fetches a value stored as **a boxed value** - a full language object with its own header and a pointer to it - works out what type it holds, finds the multiplication for that type, does the arithmetic, allocates a new object for the answer and stores a pointer to it. In **a whole-column operator** - an operation the library provides that takes a whole column and loops over it inside compiled code - the column's single **stored representation** is known before the loop starts: every value shares one representation and one fixed width. The compiled loop therefore reads raw bytes at a fixed stride, multiplies, writes raw bytes, and does nothing else per step. | | A loop you write over records | One whole-column expression | |---|---|---| | Dispatches your program makes | one per value | one in total | | Type decision | once per value | once, before the loop starts | | Value as the arithmetic sees it | a boxed language object | raw bytes of a known width | | Where the million steps run | in your program | in the library's compiled pass | | Result | assembled value by value | one buffer allocated up front | ## Two things it does not do 1. **It does not make the work disappear.** A million multiplications still happen. If you need an answer for a million rows you pay for a million multiplications either way; what you stop paying for is a million decisions about *how* to multiply. 2. **It does not, by itself, use more than one core.** Designs built on a multi-threaded execution engine do split a column across threads. The classic packed-buffer designs run a single-threaded compiled loop and still beat the record loop by a wide margin. Credit the win to removing per-value dispatch first, and treat threading as a second, separate multiplier that some designs offer and others do not. ## The exception to "the operands are untouched" The plain operator allocates and leaves its operands alone, which is what makes an expression safe to write in the middle of a longer computation. Two exceptions are worth carrying: - the **in-place** operator forms write into a buffer that already exists, so anything else holding a reference to that buffer sees the change; - a result that shares an operand's memory rather than owning its own will carry a later write back into the original. If you are relying on an operand still holding its old values three lines later, use the plain form and accept the allocation. ## How to sanity-check the claim yourself 1. Time both forms on the same data at a few sizes - a thousand values, a hundred thousand, ten million - and look at the *ratio*, not the absolute times. If the ratio grows with length, the per-value cost is what you removed. 2. Check the length of what came back. If it is one number, you wrote a fold, not an element-wise expression. 3. Print an operand after the expression. If it changed, you used an in-place form, not the plain one. ## What to say in an interview Name the three facts - a new column, one result per position, one dispatch from your program - then immediately say that the loop moved rather than vanished, and say what one step of it now costs. That last sentence is what separates a candidate reciting a slogan from one who knows the mechanism.

  • If the result is a brand new column of a million values, what did the expression cost in memory?
    One result-sized buffer, on top of the two operands, which are all still live while it is being filled. Over eight-byte values that is roughly eight megabytes for a million rows. The operands are not released by the expression, so at the moment the result is complete all three exist.
  • What is still being done a million times after the rewrite?
    The arithmetic itself, plus the loads and stores around it. The compiled pass steps through the operands' bytes at a fixed stride and writes one result per step. The saving is entirely in what each step no longer has to do: no unwrapping, no type decision, no per-value allocation.
  • Does a plain element-wise operator ever change one of its operands?
    The plain form does not - it allocates a result and writes only there. The in-place forms do: they write into a buffer that already exists, which is faster and allocates nothing, but is visible through every other reference to that buffer. Reach for the in-place form deliberately, not by habit.

Ordering a thousand identical parts on one purchase order instead of a thousand separate orders. The warehouse still picks a thousand parts - that work is unchanged - but you stop paying for a thousand lots of paperwork, approvals and type-of-item decisions. And the goods arrive as a new pallet; your original stock is still where it was.

saying these in an interview costs you the question

  • Says the per-value loop disappears entirely rather than moving into compiled code
  • Expects a single number back instead of a column of a million results
  • Assumes the plain operator writes its answer back into the left operand
  • Attributes the whole speed-up to using more of the machine's cores
  • Thinks one dispatch means one machine instruction covers the whole column
  • Believes the new column is free because no explicit allocation was written
open as a page

Why does adding a single number to a 1,000-value column behave differently from adding a 3-value operand to it?

level: juniorimportance: must knowfreq 68%

basics

~20 s

A one-value operand has nothing to disagree about: every rule reuses it at every position, giving 1,000 results. Three values against 1,000 positions is a real size conflict, and designs resolve it differently - by refusing, by repeating, or by filling with absent values.

open as a page

A column of 200,000 customer names is folded to one case and trimmed, yet a later check still sees the old values — why?

level: juniorimportance: must knowfreq 70%

basics

~20 s

The ordinary whole-column text operation computes a new column and returns it; it does not edit the values where they sit. If nothing captures the result, or it is captured under a name the later check does not read, the table keeps the old values and no error is raised.

open as a page

Doubling two million numbers by looping over records and assigning each result back: what is paid once per record?

level: juniorimportance: must knowfreq 85%

basics

~20 s

Four fixed overheads move from once-per-column to once-per-record: a run-time type decision per value, each value handed back as a wrapped language object, a fresh record built to reach its fields, and a result container reallocated as it grows.

open as a page

A colleague replaced a per-row loop by handing that same function body to a column surface. What decides whether the loop is really gone?

level: juniorimportance: must knowfreq 68%

basics

~20 s

What the surface hands the body. Called once per record, it is the same loop with a crossing into your code at every record. Called once with the whole column, it is one hand-off and the work stays inside compiled code.

open as a page

An element-wise sum of two columns: what lines the operands up when they carry row labels, and when they do not?

level: middleimportance: must knowfreq 66%

basics

~20 s

It depends on what the operands carry. Bare packed buffers match strictly by position, offset against offset, so the stored order is load-bearing. Operands that carry row labels are matched on those labels instead, over the union of both sides.

open as a page

Why does subtracting a 3-value single-row operand from a 4-value single-column operand return 12 numbers instead of an error?

level: middleimportance: must knowfreq 60%

basics

~20 s

Under a rule that lines the two operands' axis lengths up from the last axis backwards and stretches any axis of length one, both operands qualify: one is four by one, the other one by three. Both stretch, and the result is a four-by-three rectangle of every pairing.

open as a page

A loop builds a result by combining the accumulated table with one more row each iteration, so why does it slow down as it runs?

level: middleimportance: must knowfreq 68%

basics

~20 s

Where each combination materialises a new result, it copies everything accumulated so far, so iteration k copies k-1 rows and total work grows with the square of the row count. Collect the rows and combine once instead.

open as a page

A per-row loop over a 50-row column is rewritten as one whole-column expression and gets slower — why?

level: middleimportance: must knowfreq 60%

basics

~20 s

Every whole-column call pays fixed setup before touching a value: checking arguments, resolving types, allocating the result. Over 50 values that setup costs more than the loop it replaced; over a million it disappears into the per-value work.

open as a page

A function you hand the library does one multiplication per record yet dominates runtime over ten million rows. What is paid at each record?

level: middleimportance: must knowfreq 60%

basics

~20 s

The boundary, not the arithmetic. Each record pays a call into your code and back, a conversion of the stored value into something your code can hold, a conversion of what you return, and a place in the collected result.

open as a page

Arithmetic over a 5,000,000-row column takes milliseconds while the same-shaped whole-column text normalisation takes half a minute — why?

level: seniorimportance: must knowfreq 64%

basics

~20 s

A column-shaped text surface is a way of writing the job, not a promise about how it runs. Where text is held as packed bytes with an offset per row, compiled code walks it once; where each value is a separate language object, the same-looking call is one call per value — the loop you thought you had removed.

open as a page

A record loop that fitted in memory fails after being rewritten as one whole-column chain — what changed?

level: seniorimportance: must knowfreq 56%

basics

~20 s

The loop held one record at a time; the chain holds whole columns. Under eager evaluation each operator allocates a full-length result, so several column-sized intermediates are live at once. The repair is a deliberate loop over blocks of rows.

open as a page

Why does a whole-column comparison over ten million values do more work than a loop that stops at the first match?

level: juniorimportance: should knowfreq 46%

basics

~20 s

A whole-column expression produces one result per position, so it computes all ten million before anything reads them. A loop that returns at the first match does work proportional to where that match sits — sometimes three values.

open as a page

A chain of four arithmetic operators runs over a ten-million-row column - how many full-length results get allocated?

level: middleimportance: should knowfreq 54%

basics

~20 s

Under eager evaluation, four: each operator returns a complete ten-million-value result before the next operator reads it, so three intermediates exist on the way to the answer. Under deferred evaluation the chain is one recorded expression and the middles need never be built.

open as a page

When a smaller operand is stretched across a larger one, what is actually allocated - the repeat, the result, or both?

level: middleimportance: should knowfreq 38%

basics

~20 s

The result always. The repeat need not be: an implementation can walk the larger operand while re-reading the same stored value at each step, so the stretch allocates nothing. Implementations that materialise the repeat first pay for both.

open as a page

When a column of address lines is split on a separator into several columns, what fixes how many columns come back?

level: middleimportance: should knowfreq 52%

basics

~20 s

Nothing in a single row can fix it: the result is a rectangle, so one width must cover every row. Designs resolve that by scanning the column and taking the widest row, by making you declare the width or the output names, or by refusing ragged input outright.

open as a page

Why does a surface that hands back one whole record per iteration cost more than reading only the two fields you need?

level: middleimportance: should knowfreq 52%

basics

~20 s

A record does not exist in memory: columns are stored separately, so each iteration gathers one value from every column, wraps each, and builds a container. You pay for the table's full width even when the body reads two fields.

open as a page

A per-record body applies one rate above a threshold and another below it. How do you express that branch as whole-column work?

level: middleimportance: should knowfreq 50%

basics

~20 s

Compute both alternatives over the whole column, build the comparison as a condition of one value per position, then choose per position. The branch becomes data instead of control flow, and no caller-supplied body is invoked at all.

open as a page

The same threshold comparison over a column with absent values yields a two-state result on one tool and a three-state result on another - why?

level: seniorimportance: should knowfreq 43%

basics

~20 s

Because the two tools represent absence differently. Where absence is a borrowed floating-point sentinel, every comparison against it is simply false, so the condition column has two states. Where absence is tracked separately, the comparison yields absence and a third state travels onward.

open as a page

A team rewrites a per-record loop as whole-column expressions, gets 60x, and credits multi-core execution - what actually changed?

level: seniorimportance: should knowfreq 48%

basics

~20 s

Almost certainly not the core count. The classic packed-buffer pass is single-threaded; the gain is one dispatch over typed bytes in place of an unwrap, a type decision and a rewrap per value. Some engines do additionally split a column across threads, and that is a separate multiplier.

open as a page

A transform subtracting a shorter operand from a longer one is moved to a different tool, still runs, but returns different numbers. What could the second tool have done?

level: seniorimportance: should knowfreq 42%

basics

~20 s

It resolved the size conflict by a different rule. The same written expression can return a rectangle of every pairing, a result filled by repeating the shorter operand, or a result over both sets of row labels that is mostly absent - and only one of those three designs refuses outright.

open as a page

A pattern extraction over a 2,000,000-row text column raises nothing, yet 340,000 rows come back with no value — what do you check?

level: seniorimportance: should knowfreq 58%

basics

~20 s

Extraction over a column is total, not validating: a row the pattern does not fit yields no value rather than an error, so misses are silent. Check how many of those rows held no value beforehand, sample the rest, and put a bound on the miss rate so the next run fails loudly.

open as a page

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%

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.

open as a page

A step where each row's value depends on the previous row's result — when does that still have a whole-column form?

level: seniorimportance: should knowfreq 52%

basics

~20 s

A carry that folds under an associative operator — addition, maximum, last-known-value — is a prefix scan: it splits and recombines, so one compiled pass can produce it. A carry whose next state is an arbitrary function of the previous one cannot.

open as a page

After a per-record branch was rewritten to compute both alternatives and choose per position, the step now warns on rows the branch never reached. What happened?

level: seniorimportance: should knowfreq 42%

basics

~20 s

The branch was a guard. It stopped one expression from ever seeing the rows it could not handle. The chooser has no such power: both alternatives are evaluated at every position first, so the guarded side now runs on exactly the inputs it was protected from.

open as a page

Two totals of the same floating-point column, one accumulated strictly in order and one block-by-block, disagree — why?

level: seniorimportance: nice to knowfreq 34%

basics

~20 s

Floating-point addition rounds at every step, so it is not associative: regrouping the additions changes which roundings happen. A block-at-a-time total keeps partial sums of similar magnitude and usually lands closer to the exact answer than a long sequential one.

open as a page

Your team's review rule says never hand your own function to the data library. As the lead, what is wrong with it and what replaces it?

level: principalimportance: nice to knowfreq 30%

basics

~20 s

It bans the wrong thing. Cost comes from what the surface hands the body and from the row count, not from callbacks as a category. Replace it with a rule about the contract and the scale, plus a documented escape hatch.

open as a page