无法重现 cpu 缓存未命中
Can't reproduce cpu cache-miss
美好的一天!
我正在阅读这篇精彩的文章:What every programmer should know about memory。现在我正试图弄清楚 CPU 缓存是如何工作的,并重现缓存未命中的实验。目的是在访问数据量增加时重现性能下降(图 3.4)。我写了一个小程序,应该可以重现退化,但它没有。分配超过4Gb的内存后出现性能下降,我不明白为什么。我认为它应该在分配 12 或 100 MB 时出现。也许程序是错误的,我错过了什么?我用
Intel Core i7-2630QM
L1: 256Kb
L2: 1Mb
L3: 6Mb
这是 GO 列表。
main.go
package main
import (
"fmt"
"math/rand"
)
const (
n0 = 1000
n1 = 100000
)
func readInt64Time(slice []int64, idx int) int64
func main() {
ss := make([][]int64, n0)
for i := range ss {
ss[i] = make([]int64, n1)
for j := range ss[i] {
ss[i][j] = int64(i + j)
}
}
var t int64
for i := 0; i < n0; i++ {
for j := 0; j < n1; j++ {
t0 := readInt64Time(ss[i], rand.Intn(n1))
if t0 <= 0 {
panic(t0)
}
t += t0
}
}
fmt.Println("Avg time:", t/int64(n0*n1))
}
main.s
// func readInt64Time(slice []int64, idx int) int64
TEXT ·readInt64Time(SB),[=13=]-40
MOVQ slice+0(FP), R8
MOVQ idx+24(FP), R9
RDTSC
SHLQ , DX
ORQ DX, AX
MOVQ AX, R10
MOVQ (R8)(R9*8), R8 // Here I'm reading the memory
RDTSC
SHLQ , DX
ORQ DX, AX
SUBQ R10, AX
MOVQ AX, ret+32(FP)
RET
对于那些感兴趣的人。我重现了 'cache-miss' 行为。但性能下降并不像文章描述的那样剧烈。这是最终的基准列表:
main.go
package main
import (
"fmt"
"math/rand"
"runtime"
"runtime/debug"
)
func readInt64Time(slice []int64, idx int) int64
const count = 2 << 25
func measure(np uint) {
n := 2 << np
s := make([]int64, n)
for i := range s {
s[i] = int64(i)
}
t := int64(0)
n8 := n >> 3
for i := 0; i < count; i++ {
// Intex is 64 byte aligned, since cache line is 64 byte
t0 := readInt64Time(s, rand.Intn(n8)<<3)
t += t0
}
fmt.Printf("Allocated %d Kb. Avg time: %v\n",
n/128, t/count)
}
func main() {
debug.SetGCPercent(-1) // To eliminate GC influence
for i := uint(10); i < 27; i++ {
measure(i)
runtime.GC()
}
}
main_amd64.s
// func readInt64Time(slice []int64, idx int) int64
TEXT ·readInt64Time(SB),[=11=]-40
MOVQ slice+0(FP), R8
MOVQ idx+24(FP), R9
RDTSC
SHLQ , DX
ORQ DX, AX
MOVQ AX, R10
MOVQ (R8)(R9*8), R11 // Read memory
MOVQ [=11=], (R8)(R9*8) // Write memory
RDTSC
SHLQ , DX
ORQ DX, AX
SUBQ R10, AX
MOVQ AX, ret+32(FP)
RET
我禁用了垃圾收集器以消除其影响并进行了 64B 索引对齐,因为我的处理器有 64B 缓存行。
基准测试结果是:
Allocated 16 Kb. Avg time: 22
Allocated 32 Kb. Avg time: 22
Allocated 64 Kb. Avg time: 22
Allocated 128 Kb. Avg time: 22
Allocated 256 Kb. Avg time: 22
Allocated 512 Kb. Avg time: 23
Allocated 1024 Kb. Avg time: 23
Allocated 2048 Kb. Avg time: 24
Allocated 4096 Kb. Avg time: 25
Allocated 8192 Kb. Avg time: 29
Allocated 16384 Kb. Avg time: 31
Allocated 32768 Kb. Avg time: 33
Allocated 65536 Kb. Avg time: 34
Allocated 131072 Kb. Avg time: 34
Allocated 262144 Kb. Avg time: 35
Allocated 524288 Kb. Avg time: 35
Allocated 1048576 Kb. Avg time: 39
我 运行 这个长凳很多次,每次 运行 都给了我相似的结果。如果我从 asm 代码中删除了读写操作,那么我得到了 22 个用于所有分配的周期,所以这个时间差就是内存访问时间。如您所见,第一个时间偏移是 512 Kb 分配大小。只有一个 cpu 周期,但它非常稳定。下一次更改为 2 Mb。在 8 Mb 时有最显着的时间变化,但它仍然是 4 个周期,我们完全没有缓存。
在所有这些测试之后,我发现缓存未命中并没有太大的成本。它仍然很重要,因为时差是 10-15 倍,而不是我们在文章中看到的 50-500 倍。也许今天的内存比 7 年前快得多?看起来很有希望 =) 也许在接下来的 7 年之后,将会出现完全没有 cpu 缓存的架构。我们拭目以待。
编辑:
正如@Leeor 所提到的,RDTSC 指令没有序列化行为,并且可能会乱序执行。我改用 RDTSCP 指令:
main_amd64.s
// func readInt64Time(slice []int64, idx int) int64
TEXT ·readInt64Time(SB),[=13=]-40
MOVQ slice+0(FP), R8
MOVQ idx+24(FP), R9
BYTE [=13=]x0F; BYTE [=13=]x01; BYTE [=13=]xF9; // RDTSCP
SHLQ , DX
ORQ DX, AX
MOVQ AX, R10
MOVQ (R8)(R9*8), R11 // Read memory
MOVQ [=13=], (R8)(R9*8) // Write memory
BYTE [=13=]x0F; BYTE [=13=]x01; BYTE [=13=]xF9; // RDTSCP
SHLQ , DX
ORQ DX, AX
SUBQ R10, AX
MOVQ AX, ret+32(FP)
RET
这里是我得到的变化:
Allocated 16 Kb. Avg time: 27
Allocated 32 Kb. Avg time: 27
Allocated 64 Kb. Avg time: 28
Allocated 128 Kb. Avg time: 29
Allocated 256 Kb. Avg time: 30
Allocated 512 Kb. Avg time: 34
Allocated 1024 Kb. Avg time: 42
Allocated 2048 Kb. Avg time: 55
Allocated 4096 Kb. Avg time: 120
Allocated 8192 Kb. Avg time: 167
Allocated 16384 Kb. Avg time: 173
Allocated 32768 Kb. Avg time: 189
Allocated 65536 Kb. Avg time: 201
Allocated 131072 Kb. Avg time: 215
Allocated 262144 Kb. Avg time: 224
Allocated 524288 Kb. Avg time: 242
Allocated 1048576 Kb. Avg time: 281
现在我看到了 cahce 和 RAM 访问之间的巨大差异。时间实际上比文章中的时间低 2 倍,但它是可以预测的,因为内存频率高出两倍。
这确实不会产生尝试观察,不清楚您的基准究竟做了什么 - 您是否在范围内随机访问?您在测量每次访问的访问延迟吗?
您的基准测试似乎会在每次未摊销的时间测量中产生恒定的开销,因此您基本上是在测量函数调用时间(这是常数)。
只有当内存延迟变得足够大以传递该开销时(当您以 4GB 访问 DRAM 时),您才真正开始获得有意义的测量值。
您应该切换到测量整个循环(超过 count
次迭代)和除法的时间。
美好的一天!
我正在阅读这篇精彩的文章:What every programmer should know about memory。现在我正试图弄清楚 CPU 缓存是如何工作的,并重现缓存未命中的实验。目的是在访问数据量增加时重现性能下降(图 3.4)。我写了一个小程序,应该可以重现退化,但它没有。分配超过4Gb的内存后出现性能下降,我不明白为什么。我认为它应该在分配 12 或 100 MB 时出现。也许程序是错误的,我错过了什么?我用
Intel Core i7-2630QM
L1: 256Kb
L2: 1Mb
L3: 6Mb
这是 GO 列表。
main.go
package main
import (
"fmt"
"math/rand"
)
const (
n0 = 1000
n1 = 100000
)
func readInt64Time(slice []int64, idx int) int64
func main() {
ss := make([][]int64, n0)
for i := range ss {
ss[i] = make([]int64, n1)
for j := range ss[i] {
ss[i][j] = int64(i + j)
}
}
var t int64
for i := 0; i < n0; i++ {
for j := 0; j < n1; j++ {
t0 := readInt64Time(ss[i], rand.Intn(n1))
if t0 <= 0 {
panic(t0)
}
t += t0
}
}
fmt.Println("Avg time:", t/int64(n0*n1))
}
main.s
// func readInt64Time(slice []int64, idx int) int64
TEXT ·readInt64Time(SB),[=13=]-40
MOVQ slice+0(FP), R8
MOVQ idx+24(FP), R9
RDTSC
SHLQ , DX
ORQ DX, AX
MOVQ AX, R10
MOVQ (R8)(R9*8), R8 // Here I'm reading the memory
RDTSC
SHLQ , DX
ORQ DX, AX
SUBQ R10, AX
MOVQ AX, ret+32(FP)
RET
对于那些感兴趣的人。我重现了 'cache-miss' 行为。但性能下降并不像文章描述的那样剧烈。这是最终的基准列表:
main.go
package main
import (
"fmt"
"math/rand"
"runtime"
"runtime/debug"
)
func readInt64Time(slice []int64, idx int) int64
const count = 2 << 25
func measure(np uint) {
n := 2 << np
s := make([]int64, n)
for i := range s {
s[i] = int64(i)
}
t := int64(0)
n8 := n >> 3
for i := 0; i < count; i++ {
// Intex is 64 byte aligned, since cache line is 64 byte
t0 := readInt64Time(s, rand.Intn(n8)<<3)
t += t0
}
fmt.Printf("Allocated %d Kb. Avg time: %v\n",
n/128, t/count)
}
func main() {
debug.SetGCPercent(-1) // To eliminate GC influence
for i := uint(10); i < 27; i++ {
measure(i)
runtime.GC()
}
}
main_amd64.s
// func readInt64Time(slice []int64, idx int) int64
TEXT ·readInt64Time(SB),[=11=]-40
MOVQ slice+0(FP), R8
MOVQ idx+24(FP), R9
RDTSC
SHLQ , DX
ORQ DX, AX
MOVQ AX, R10
MOVQ (R8)(R9*8), R11 // Read memory
MOVQ [=11=], (R8)(R9*8) // Write memory
RDTSC
SHLQ , DX
ORQ DX, AX
SUBQ R10, AX
MOVQ AX, ret+32(FP)
RET
我禁用了垃圾收集器以消除其影响并进行了 64B 索引对齐,因为我的处理器有 64B 缓存行。
基准测试结果是:
Allocated 16 Kb. Avg time: 22
Allocated 32 Kb. Avg time: 22
Allocated 64 Kb. Avg time: 22
Allocated 128 Kb. Avg time: 22
Allocated 256 Kb. Avg time: 22
Allocated 512 Kb. Avg time: 23
Allocated 1024 Kb. Avg time: 23
Allocated 2048 Kb. Avg time: 24
Allocated 4096 Kb. Avg time: 25
Allocated 8192 Kb. Avg time: 29
Allocated 16384 Kb. Avg time: 31
Allocated 32768 Kb. Avg time: 33
Allocated 65536 Kb. Avg time: 34
Allocated 131072 Kb. Avg time: 34
Allocated 262144 Kb. Avg time: 35
Allocated 524288 Kb. Avg time: 35
Allocated 1048576 Kb. Avg time: 39
我 运行 这个长凳很多次,每次 运行 都给了我相似的结果。如果我从 asm 代码中删除了读写操作,那么我得到了 22 个用于所有分配的周期,所以这个时间差就是内存访问时间。如您所见,第一个时间偏移是 512 Kb 分配大小。只有一个 cpu 周期,但它非常稳定。下一次更改为 2 Mb。在 8 Mb 时有最显着的时间变化,但它仍然是 4 个周期,我们完全没有缓存。
在所有这些测试之后,我发现缓存未命中并没有太大的成本。它仍然很重要,因为时差是 10-15 倍,而不是我们在文章中看到的 50-500 倍。也许今天的内存比 7 年前快得多?看起来很有希望 =) 也许在接下来的 7 年之后,将会出现完全没有 cpu 缓存的架构。我们拭目以待。
编辑: 正如@Leeor 所提到的,RDTSC 指令没有序列化行为,并且可能会乱序执行。我改用 RDTSCP 指令:
main_amd64.s
// func readInt64Time(slice []int64, idx int) int64
TEXT ·readInt64Time(SB),[=13=]-40
MOVQ slice+0(FP), R8
MOVQ idx+24(FP), R9
BYTE [=13=]x0F; BYTE [=13=]x01; BYTE [=13=]xF9; // RDTSCP
SHLQ , DX
ORQ DX, AX
MOVQ AX, R10
MOVQ (R8)(R9*8), R11 // Read memory
MOVQ [=13=], (R8)(R9*8) // Write memory
BYTE [=13=]x0F; BYTE [=13=]x01; BYTE [=13=]xF9; // RDTSCP
SHLQ , DX
ORQ DX, AX
SUBQ R10, AX
MOVQ AX, ret+32(FP)
RET
这里是我得到的变化:
Allocated 16 Kb. Avg time: 27
Allocated 32 Kb. Avg time: 27
Allocated 64 Kb. Avg time: 28
Allocated 128 Kb. Avg time: 29
Allocated 256 Kb. Avg time: 30
Allocated 512 Kb. Avg time: 34
Allocated 1024 Kb. Avg time: 42
Allocated 2048 Kb. Avg time: 55
Allocated 4096 Kb. Avg time: 120
Allocated 8192 Kb. Avg time: 167
Allocated 16384 Kb. Avg time: 173
Allocated 32768 Kb. Avg time: 189
Allocated 65536 Kb. Avg time: 201
Allocated 131072 Kb. Avg time: 215
Allocated 262144 Kb. Avg time: 224
Allocated 524288 Kb. Avg time: 242
Allocated 1048576 Kb. Avg time: 281
现在我看到了 cahce 和 RAM 访问之间的巨大差异。时间实际上比文章中的时间低 2 倍,但它是可以预测的,因为内存频率高出两倍。
这确实不会产生尝试观察,不清楚您的基准究竟做了什么 - 您是否在范围内随机访问?您在测量每次访问的访问延迟吗?
您的基准测试似乎会在每次未摊销的时间测量中产生恒定的开销,因此您基本上是在测量函数调用时间(这是常数)。 只有当内存延迟变得足够大以传递该开销时(当您以 4GB 访问 DRAM 时),您才真正开始获得有意义的测量值。
您应该切换到测量整个循环(超过 count
次迭代)和除法的时间。