Cache Lines Beat Clever Loops
I translated a matrix filter from C to Go and then spent an afternoon optimizing arithmetic that was not the main cost. The profile pointed at memory movement, so I reduced the problem to traversal order.
The matrix is stored in one slice in row-major order. These loops calculate the same sum:
for y := 0; y < height; y++ {
for x := 0; x < width; x++ {
sum += pixels[y*width+x]
}
}
for x := 0; x < width; x++ {
for y := 0; y < height; y++ {
sum += pixels[y*width+x]
}
}
The first walks adjacent elements. The second jumps by an entire row. On a large image the row-wise loop is much faster because each fetched cache line supplies several values used immediately. The column-wise loop asks the processor to fetch many lines and uses a small part of each before moving on.
Bounds checks and multiplication were my original suspects. Hoisting the row offset made the code clearer and helped a little:
for y := 0; y < height; y++ {
row := pixels[y*width:(y+1)*width]
for x := 0; x < width; x++ {
sum += row[x]
}
}
But traversal order remained the large effect. A more sophisticated arithmetic trick could not compensate for unfriendly access.
I benchmark with enough data to exceed the small caches, repeat the operation, and verify the sum so the compiler cannot discard the work. These are 2011 compiler snapshots, so absolute timings and optimization behavior are temporary facts. The memory hierarchy is less temporary.
I alternate test order as well. Running one version first every time can give it consistently colder data and turn benchmark order into a hidden input.
This also affects data structure choice. A slice of small structures may have better locality than a slice of pointers to separately allocated structures. The latter adds pointer chasing and gives the allocator freedom to scatter objects. It may still be right when identity and independent lifetimes matter, but it is not free.
My practical conclusion is embarrassingly physical: visit data in the order it lies in memory. Before unrolling loops or translating a bit trick from C, inspect the access pattern. The processor is very fast when fed and becomes an expensive space heater when asked to wait.