liveBook · Manning

Spatial locality

As explained in 100 Go Mistakes and How to Avoid Them

Spatial locality is the tendency for memory locations near a recently accessed location to be accessed soon.

A common optimization mistake is to count only the values a loop reads, not the memory fetched to reach them. Spatial locality is the tendency to access nearby memory locations soon after one another. A CPU typically copies a contiguous block into a cache line rather than fetching one variable at a time. A cache line is usually 64 bytes, enough for eight int64 values. section 12.1.2

func sum(s []int64) int64 {
    var total int64
    length := len(s)
    for i := 0; i < length; i++ {
        total += s[i]
    }
    return total
}

This loop reads a contiguous slice in order. Fetching the line containing s[0] can also make nearby elements available; repeatedly reading total instead illustrates temporal locality. section 12.1.2

Keep the fields you read together

Suppose a loop sums only the a field. With `type Foo struct { a, b int64 }` and `[]Foo`, each a sits beside an unused b. With `type Bar struct { a, b []int64 }`, the a values occupy a contiguous slice. In the comparison, summing Bar.a required two cache lines rather than four and ran about 20% faster on the measured machine. The point is not that a struct of slices is always better: choose a layout around the fields a hot loop actually uses. section 12.1.3

Locality alone does not explain every result. Reading every slice element has a predictable unit stride; reading every second element has a predictable constant stride. Following linked-list pointers has a non-unit stride that the CPU cannot predict from the addresses alone. In the cited comparison, a slice loop was about 70% faster than a linked-list traversal despite similar spatial locality. section 12.1.4

Check padding before blaming the loop

Field order can change how many useful values fit in a line. `struct { b1 byte; i int64; b2 byte }` occupies 24 bytes in the example; moving i first reduces it to 16 bytes. A loop summing i across 10,000 structs ran about 15% faster with the compact layout on the measured machine. Benchmark the relevant workload rather than treating either figure as a universal gain. section 12.4

How 100 Go Mistakes and How to Avoid Them covers it

How deep it goes
Defines locality and cache lines, then compares layouts and access strides; stops short of a universal layout rule.
Examples
Compares contiguous slice sums, slices of structs against structs of slices, linked-list traversal, and reordered struct fields.
What it assumes
Assumes familiarity with Go slices, structs, pointers, and loops while defining spatial locality and cache lines.
What you can do after
Evaluate/understand it by comparing cache-line use and access predictability before changing a Go data layout.
How it connects
Connects cache lines and temporal locality to access strides, struct padding, and data alignment.

Related concepts

This explainer was generated by Manning's knowledge graph from the text of 100 Go Mistakes and How to Avoid Them. The section links point to the parts of the book it draws on.