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
}
关键优化原则:
- 保持数据紧凑,减少填充
- 顺序访问内存,避免随机访问
- 利用空间局部性,相关数据放在一起
- 预分配内存,避免动态扩容
- 批量处理数据,减少缓存失效
使用go test -bench . -benchmem可以观察内存分配和缓存效率。实际性能提升取决于具体硬件架构和数据访问模式。

