Golang如何编写缓存友好的代码?

Golang如何编写缓存友好的代码? 大家好,

能否为我提供一些考虑到缓存友好性的代码示例?或许还有一些基准测试和实际案例?

3 回复

请查看 https://www.youtube.com/watch?v=YDPKUJndhw4 中关于链表与切片的示例。

更多关于Golang如何编写缓存友好的代码?的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html


@amnon

那是一次很棒的演讲,真的启发了我。感谢你的分享。

你能给我一些思路,在哪里可以找到更多关于缓存友好性的例子吗?最好更贴近我们日常编写的代码。

另外,你提到我们有三种工具可以改进:嵌入、切片和更小的数据类型。你能分享一些关于如何衡量每种方法的效率,以及如何找到更多工具和技巧的想法吗?

在Go中编写缓存友好的代码主要涉及数据局部性和内存访问模式的优化。以下是一些关键技术和示例:

1. 数据布局优化

结构体字段重排

// 不友好的布局 - 导致缓存行浪费
type BadLayout struct {
    active bool      // 1字节
    id     int64     // 8字节
    name   [32]byte  // 32字节
    status int32     // 4字节
}

// 友好的布局 - 减少填充,提高空间局部性
type GoodLayout struct {
    id     int64     // 8字节
    name   [32]byte  // 32字节
    status int32     // 4字节
    active bool      // 1字节
    _      [3]byte   // 填充到8字节对齐
}

数组结构体 vs 结构体数组

// AOS(结构体数组) - 适合顺序访问所有字段
type PointAOS struct {
    X, Y, Z float64
}

func processAOS(points []PointAOS) float64 {
    var sum float64
    for i := range points {
        sum += points[i].X + points[i].Y + points[i].Z
    }
    return sum
}

// SOA(数组结构体) - 适合只访问部分字段
type PointsSOA struct {
    Xs, Ys, Zs []float64
}

func processSOA(points PointsSOA) float64 {
    var sum float64
    for i := range points.Xs {
        sum += points.Xs[i] + points.Ys[i] + points.Zs[i]
    }
    return sum
}

2. 循环优化

循环分块(Loop Tiling)

const blockSize = 16 // 通常设为缓存行大小/元素大小

func matrixMultiply(a, b, c [][]float64) {
    n := len(a)
    for i := 0; i < n; i += blockSize {
        for j := 0; j < n; j += blockSize {
            for k := 0; k < n; k += blockSize {
                // 处理块
                for ii := i; ii < min(i+blockSize, n); ii++ {
                    for kk := k; kk < min(k+blockSize, n); kk++ {
                        aik := a[ii][kk]
                        for jj := j; jj < min(j+blockSize, n); jj++ {
                            c[ii][jj] += aik * b[kk][jj]
                        }
                    }
                }
            }
        }
    }
}

3. 预取和预分配

对象池和复用

type ObjectPool struct {
    pool sync.Pool
}

func (p *ObjectPool) Get() *Buffer {
    buf := p.pool.Get()
    if buf == nil {
        return &Buffer{data: make([]byte, 0, 4096)}
    }
    return buf.(*Buffer)
}

func (p *ObjectPool) Put(buf *Buffer) {
    buf.data = buf.data[:0]
    p.pool.Put(buf)
}

预分配切片

// 不好的做法 - 多次扩容
func processBad(data []int) []int {
    var result []int
    for _, v := range data {
        if v%2 == 0 {
            result = append(result, v*2)
        }
    }
    return result
}

// 好的做法 - 预分配
func processGood(data []int) []int {
    result := make([]int, 0, len(data))
    for _, v := range data {
        if v%2 == 0 {
            result = append(result, v*2)
        }
    }
    return result
}

4. 基准测试示例

func BenchmarkCacheFriendly(b *testing.B) {
    const size = 10000
    data := make([]int, size)
    for i := range data {
        data[i] = i
    }
    
    b.Run("ColumnMajor", func(b *testing.B) {
        matrix := make([][]int, size)
        for i := range matrix {
            matrix[i] = make([]int, size)
        }
        
        b.ResetTimer()
        for n := 0; n < b.N; n++ {
            sum := 0
            // 列优先访问 - 不友好
            for col := 0; col < size; col++ {
                for row := 0; row < size; row++ {
                    sum += matrix[row][col]
                }
            }
            _ = sum
        }
    })
    
    b.Run("RowMajor", func(b *testing.B) {
        matrix := make([][]int, size)
        for i := range matrix {
            matrix[i] = make([]int, size)
        }
        
        b.ResetTimer()
        for n := 0; n < b.N; n++ {
            sum := 0
            // 行优先访问 - 友好
            for row := 0; row < size; row++ {
                for col := 0; col < size; col++ {
                    sum += matrix[row][col]
                }
            }
            _ = sum
        }
    })
}

5. 实际案例:B+树节点优化

const (
    nodeSize    = 256
    cacheLine   = 64
    keySize     = 8
    childSize   = 8
)

// 优化后的B+树节点布局
type BPlusNode struct {
    count   uint16
    leaf    bool
    _       [cacheLine - 2 - 2]byte // 填充到缓存行边界
    keys    [nodeSize]uint64
    children [nodeSize + 1]uintptr
}

// 批量处理叶子节点数据
func processLeafRange(leaves []BPlusNode, start, end int) {
    // 确保访问连续内存区域
    for i := start; i < end; i++ {
        node := &leaves[i]
        for j := 0; j < int(node.count); j++ {
            // 顺序访问keys
            _ = node.keys[j]
        }
    }
}

6. 使用内置函数优化

// 利用copy进行内存块操作
func copyBlocks(dst, src []byte, blockSize int) {
    for i := 0; i < len(src); i += blockSize {
        end := i + blockSize
        if end > len(src) {
            end = len(src)
        }
        copy(dst[i:end], src[i:end])
    }
}

// 使用binary包处理对齐数据
func processAlignedData(data []byte) uint64 {
    var sum uint64
    for i := 0; i < len(data); i += 8 {
        if i+8 <= len(data) {
            sum += binary.LittleEndian.Uint64(data[i:])
        }
    }
    return sum
}

关键优化原则:

  1. 保持数据紧凑,减少填充
  2. 顺序访问内存,避免随机访问
  3. 利用空间局部性,相关数据放在一起
  4. 预分配内存,避免动态扩容
  5. 批量处理数据,减少缓存失效

使用go test -bench . -benchmem可以观察内存分配和缓存效率。实际性能提升取决于具体硬件架构和数据访问模式。

回到顶部