liveBook · Manning

Cache line

As explained in 100 Go Mistakes and How to Avoid Them

A cache line is the fixed-size unit of contiguous memory transferred between main memory and a processor cache.

Reading one value does not necessarily fetch only that value. A cache line is a fixed-size contiguous memory block copied into a CPU cache, usually 64 bytes. This takes advantage of spatial locality: after reading one slice element, nearby elements are likely to be read soon. A first access can miss the cache, while later accesses within the fetched line can hit it. section 12.1.2

Make each fetched line useful

Consider summing the a fields of 16 records. In a slice of structs containing a and b, reads of a skip the interleaved b values and require four cache lines. In a struct containing separate a and b slices, the a values are contiguous and require two lines. The latter ran about 20% faster in the reported benchmark. Choose a layout according to which fields the work actually reads. section 12.1.3

Layout is not the only consideration; the access pattern matters too:

  • A unit stride reads contiguous values and uses the fewest lines for the walk. A constant stride, such as every second slice element, remains predictable but uses more lines. section 12.1.4
  • Following pointers in a linked list is a non-unit stride. Even when its nodes happen to be close together, the CPU cannot predict the next address as it can for a slice walk. section 12.1.4
func sum2(s []int64) int64 {
    var total int64
    for i := 0; i < len(s); i+=2 {     #1
        total += s[i]
    }
    return total
}
 
func sum8(s []int64) int64 {
    var total int64
    for i := 0; i < len(s); i += 8 {   #2
        total += s[i]
    }
    return total
}

Compare the loops that read every second or every eighth int64. Skipping values changes how much work each fetched line contributes; it does not make the transfer unit smaller. section 12.1.2

Another surprise is that reading the same eight columns can take different times when row widths change. In the reported matrix benchmark, a 513-column version was about 50% faster than a 512-column version when the matrix was reused; benchmarking with a new matrix each time showed no difference. Cache lines must be placed in a partitioned, set-associative cache, so repeated accesses can depend on placement as well as on the values read. section 12.1.5

Independent writes can still interfere

Two goroutines can update separate int64 fields without a data race yet lose performance if those fields occupy the same cache line. Each core has its own L1 data cache; a write can invalidate the entire replicated line, not merely the field changed. This is false sharing. Separating the fields with padding is a contextual fix when this pattern matters—not a reason to pad every struct. section 12.2

type Result struct {
    sumA int64
    _    [56]byte     #1
    sumB int64
}

The padded Result layout puts 56 bytes between two int64 fields, illustrating the fix for the concurrent counters. section 12.2

How 100 Go Mistakes and How to Avoid Them covers it

How deep it goes
Explains fixed-size transfers through misses, strides, cache placement, and line-level invalidation, without specifying hardware-dependent cache behavior.
Examples
Contrasts slice and struct layouts, 512- and 513-column matrices, and two goroutines updating separate fields.
What it assumes
Assumes familiarity with Go slices, structs, and goroutines while defining locality and cache lines before applying them.
What you can do after
Diagnose/debug with it when contiguous reads waste fetched data or independent concurrent writes interfere.
How it connects
Connects spatial locality to cache-line utilization and false sharing to cache-line invalidation.

Also explained in

Compare how 4 books explain cache line →

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.