Lesson 6 of 7

Delete the loop: five vectorization patterns

Five shapes of loop, and the one expression that replaces each. This is the lab the rest of the module was for: by the end of it you should be able to look at a loop and name the pattern before you have finished reading it.

Everything so far has been about reading array code. This is about writing it. Five patterns cover most of the loops in numerical Python -- an operation applied elementwise, an accumulator, an if, a nested loop, and a loop of sums -- and each one has an expression that replaces it outright.

Every board below states a loop that works and is slow, and asks for the expression that computes the same thing. Both of them ran when this page was built. The loop is executed, the candidates are evaluated over the same data, and a candidate that disagrees with its own loop is refused rather than published -- which is the only reason it is safe to tell you that these are equivalent.

Each board is a loop that works and is slow. Replace it with the expression that computes the same thing — the loop and the expression are both run here, at import, and a pattern that does not agree with its loop never reaches the page. The wrong answers are run too, and every one of them is a real mistake: an axis off by one, a keepdims missing, a transposed operand, both branches of an if the wrong way round. Some of them raise. Most of them do not, which is the harder lesson.

1 of 7

Read the loop before you delete it

Four questions about the index, and the loop names the pattern that replaces it.

The first lesson of this module said why a Python loop over an array is slow, and it is worth restating because it is the whole motivation. Every iteration is an interpreter step: follow a pointer to an object, ask that object what addition means, allocate a result, store it. The numbers are sitting in one contiguous block and nothing is walking that block -- the interpreter is walking a list of instructions instead.

Deleting the loop is not a micro-optimization. It is writing down what the operation is, once, so that a compiled routine can walk the block. The expression is usually shorter than the loop too, and that turns out to be the smaller benefit.

But you cannot delete a loop you have not read. Four questions, in this order, and between them they name the pattern:

  • What does the index touch? If i appears only as a[i] and out[i] -- the same position on both sides -- then nothing crosses between elements. Pattern one.
  • Is there an accumulator? A variable set before the loop and updated inside it is a reduction, and the loop it lives in is the axis. Pattern two.
  • Is there an if? A condition asked one element at a time is a mask over all of them. Pattern three.
  • Is the loop nested? If the two indices reach into different arrays, one operand repeats and that is broadcasting, pattern four. If both indices reach into both arrays and the innermost loop accumulates, the shared index is summed away, and that is a matrix multiply, pattern five.

Most real loops are two or three of those stacked. Read from the inside out and name each one as you meet it.

The habit that makes this safe: keep the loop. Write the expression beside it, run both on a small array, and compare before you delete anything. That is not advice about this page, it is what this page does -- every board below runs its loop and its expression when the server starts, and a pattern that disagrees with its own loop never reaches you.