skip to content

Element-Wise as the Default Unit

One expression applies to every value in a column at once, position by position, and hands back a new column. What it lines the operands up on, and what it allocates on the way, is where people slip.

on this pageshow

questions

5

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

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

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

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