Why Vectorization Beats Loops
You run Rosewood Bakery. This morning six items sit on the shelf: a croissant at $3.20, a muffin at $2.50, a bagel at $2.00, a scone at $2.80, a donut at $1.90 and a loaf at $4.50. Costs went up, so every price needs to rise 8%.
You already know how to do this with a for loop: visit price one, raise it, store it; visit price two, raise it, store it; and so on. The widget below steps a for loop one pass at a time, so you can watch what one trip per element really looks like. Step through it and watch it crawl.
This lesson is about a faster, clearer idea: tell R to raise the whole price list at once, in a single expression, with no loop and no counter. That move is called vectorization, and once you see it you will reach for it constantly.
By the end of this lesson you will be able to:
- Rewrite an element-by-element loop as a single vectorized expression
- Explain recycling: how one number, or a matching list of numbers, is applied across a whole vector
- Reach for a vectorized conditional or summary instead of a hand-written loop
- Explain why vectorized code runs far faster than the equivalent R loop
Prerequisites: you can build and name a vector, you can write a for loop and use [ifelse()](Control-Flow-in-R.html), and you know R's comparison operators like >= return TRUE/FALSE.
One operation, the whole vector at once
Here are the bakery's six prices. Each lesson runs in a fresh R session, so build them once now (run this):
To raise every price by 8% the loop way, you create a place to store the answers, then walk the prices one index at a time:
That works, but look at all the machinery: an empty vector, an index i, a seq_along(), and a manual write into slot i. Now the vectorized way, the same job in one line:
prices * 1.08 multiplies every element at once and hands back a whole new vector, names and all. No counter, no storage to set up, no index to get wrong.
This is what vectorized means precisely. If \(x = (x_1, x_2, \dots, x_n)\) is a vector of length \(n\) (here \(n = 6\)), a vectorized function \(f\) applied to \(x\) returns the length-\(n\) vector formed by applying \(f\) to each element:
\[ f(x) = \big(f(x_1),\, f(x_2),\, \dots,\, f(x_n)\big) \]
The loop is still happening, you just are not writing it. R runs it for you, in one step, over the whole vector.
Recycling: how one number meets many
In prices * 1.08, one value (1.08) met six values. How? Through R's recycling rule: when you combine vectors of different lengths, the shorter one is repeated as needed to match the longer one. A single number is length 1, so it gets reused for every element.
When two equal-length vectors meet, they line up position by position, element 1 with element 1, element 2 with element 2, and so on. Say each item has its own promo discount in dollars:
Formally, when two vectors \(x\) and \(y\) of the same length combine with an operator \(\star\), the result \(z\) has \(z_i = x_i \star y_i\) for each position \(i\). If \(y\) has length 1, that single \(y_1\) is recycled, so \(z_i = x_i \star y_1\) for every \(i\).
Recycling even works when the longer length is an exact multiple of the shorter. But if it is not a clean multiple, R still recycles and warns you, a sign your lengths probably do not mean what you think:
Raise every price, one line
You have prices, a length-6 numeric vector of shelf prices, and you want every price raised by 8% in a single expression. Which one does that correctly?
1.08 is recycled across all six elements, so you get a length-6 vector of raised prices back, no loop, counter or storage needed.sum() is a reduction, not an element-wise operation.Clearer code: say what, not how
Speed gets the headlines, but clarity is the daily win. A vectorized line reads like the sentence you would say out loud. Compare labelling each item "premium" or "everyday". The loop spells out the bookkeeping:
ifelse() is the vectorized conditional, it makes the same decision for every element at once and returns a vector of labels:
The other everyday move is a reduction: collapsing a whole vector into a single summary number. R ships these vectorized and ready, no loop required:
ifelse()) or boil the vector down to a summary (use a reduction like sum(), mean() or max()). In both cases the vectorized form is shorter and says exactly what it does.Vectorize a points calculation
Rosewood gives 10 loyalty points per dollar. Compute points for every item at once, no loop, by multiplying the whole prices vector. Fill in the blank, then check. (You should get one points value per item.)
Show answer
prices <- c(croissant = 3.20, muffin = 2.50, bagel = 2.00,
scone = 2.80, donut = 1.90, loaf = 4.50)
points <- round(prices * 10)
points
#> croissant muffin bagel scone donut loaf
#> 32 25 20 28 19 45Why it runs faster
Six prices finish instantly either way. The difference shows up at scale, so step the loop widget below once more and count: every pass is a separate trip through R's interpreter, R reads the next element, works out what to do with it, computes it, then stores the result. Five passes here. Now picture the bakery's full sales log: not six numbers but 100,000 line items this year.
A vectorized call does not make 100,000 separate trips. R recognises x * 10 as one operation on the whole vector and hands the actual element-by-element loop to fast compiled code (written in C) underneath. One trip through the interpreter, then a tight machine-level loop. The R loop pays the interpreter's overhead 100,000 times; the vectorized call pays it once. Time both on the bakery's sales log and see:
Your exact elapsed times will vary by machine, but the vectorized line is typically dozens to hundreds of times faster, while identical() confirms both produce the very same result.
c() on every pass. Each c() copies the whole vector to a bigger spot, so the cost balloons as the vector grows. If you must loop, preallocate and fill by index.Where does the speed come from?
Your vectorized x * 10 finishes almost instantly, while the equivalent preallocated for loop over the same 100,000 numbers takes far longer. Both do exactly the same multiplications. Why is the loop so much slower?
When a loop is still the right tool
Vectorization is the default, not a law. The one case it cannot replace is a genuine sequential dependence: when each value needs the value the loop just computed. There is no "whole vector at once" because element m does not exist until element m - 1 is done.
Suppose Rosewood saves up: each month the balance earns 1% interest on last month's balance, then a $200 deposit lands. Month 2 depends on month 1, month 3 on month 2, and so on, a chain. A loop expresses that chain directly:
The widget shows why no single vectorized expression does this: each box reads the box before it.
References
A few authoritative, free places to take vectorization further:
- Advanced R (2e): Improving performance, "Vectorise" - Hadley Wickham on why vectorized code is fast and how to spot loops worth replacing.
- The R Inferno (Patrick Burns) - Circle 2 ("Growing objects") and Circle 3 ("Failing to vectorize") are the classic, vivid treatment of the exact traps in this lesson.
- Hands-On Programming with R: Speed - a beginner-first chapter that builds up vectorized code and benchmarks it against loops.
- An Introduction to R: Simple manipulations - the official manual on vector arithmetic and the recycling rule.
Lesson 1 complete
You can now turn an element-by-element loop into a single vectorized expression, you understand recycling (a length-1 value reused for every element, equal-length vectors lined up position by position, and the warning when lengths do not divide evenly), you reach for ifelse() and reductions like sum() instead of writing the loop yourself, and you can say why the vectorized form is so much faster: one trip through the interpreter and a compiled inner loop, not 100,000 interpreted passes. You also know the honest exception, a genuine sequential dependence still wants a loop.
Next, Lesson 2: The apply Family. Not every "do this to each element" job is plain arithmetic, sometimes you need to run your own function over each piece of a list or each row of a matrix. apply(), lapply(), sapply() and the type-safe vapply() give you vectorization's clarity for those jobs too.