【Go语言测评】CPU cache命中对性能的影响

201 阅读3分钟

CPU cache

借用小林codeing的一幅图:

image.png CPU处理的数据是从内存一步步经过三级cache和寄存器得到的。因为CPU运算速度很快,例如1GHz,1ns执行一步,但是内存随机访问要数百ns,cpu不可能干等着内存。因此CPU内部有高速缓存,可以很快的速度读写,满足CPU的性能。

内存中的数据搬运到cache并不会一次只读当前需要的,这样就又陷入速度的困境,因此每次将一大块连续的数据约64KB,从内存搬运到chache。这样以后读取数据时,先看看chache里有没有,如果没有再去内存搬运。

image.png

代码局部性

代码局部性(code locality)指的是在程序执行过程中,对同一块数据或同一块代码的反复使用,以及这些数据或代码之间的空间或时间上的紧密关联性。

在计算机系统中,代码局部性是一种重要的性质,它可以影响程序的性能。具有良好的代码局部性意味着程序可以更有效地利用计算机的缓存和内存系统,从而提高程序的运行效率。

代码局部性可以分为以下两种类型:

  1. 时间局部性:当程序访问某个数据时,这个数据在不久的将来很可能会被再次访问。例如,循环中的变量往往在每次迭代中被访问,因此具有良好的时间局部性。
  2. 空间局部性:当程序访问某个数据时,这个数据附近的数据很可能也会被访问。例如,数组的元素通常是相邻的,因此具有良好的空间局部性。

因此,编写具有良好的代码局部性的程序是非常重要的,它可以减少内存和缓存访问的开销,从而提高程序的性能。

局部性编程对比

下面设定三种访问情况:

  1. 只访问数组一个位置,每一次访问cache都命中
  2. 顺序访问数组,有很大可能性命中
  3. 乱序访问数组,每一次都不命中
const SIZE = 1024

func Benchmark_test0(b *testing.B) {
   m := make([][]int, SIZE)
   for i := 0; i < len(m); i++ {
      m[i] = make([]int, SIZE)
   }

   b.ResetTimer()
   for i := 0; i < b.N; i++ {
      for x := 0; x < SIZE; x++ {
         for y := 0; y < SIZE; y++ {
            m[0][0] = x + y
         }
      }
   }
}

func Benchmark_test1(b *testing.B) {
   m := make([][]int, SIZE)
   for i := 0; i < len(m); i++ {
      m[i] = make([]int, SIZE)
   }

   b.ResetTimer()
   for i := 0; i < b.N; i++ {
      for x := 0; x < SIZE; x++ {
         for y := 0; y < SIZE; y++ {
            m[x][y] = x + y
         }
      }
   }
}

func Benchmark_test2(b *testing.B) {
   m := make([][]int, SIZE)
   for i := 0; i < len(m); i++ {
      m[i] = make([]int, SIZE)
   }

   b.ResetTimer()
   for i := 0; i < b.N; i++ {
      for x := 0; x < SIZE; x++ {
         for y := 0; y < SIZE; y++ {
            m[y][x] = x + y
         }
      }
   }
}

结果:

goos: windows
goarch: amd64
pkg: tmp/tmp
cpu: AMD Ryzen 7 6800H with Radeon Graphics         
Benchmark_test0-16          3082            388104 ns/op
Benchmark_test1-16          2367            510889 ns/op
Benchmark_test2-16           214           5873091 ns/op

可以看到,顺序读写数组与乱序读写的速度差距竟然有一个数量级。