我正在研究缓存对 x86-64 CPU 性能的影响。我一直在使用 Linux 的 perf 来监控缓存命中/未命中率,特别是这些计数器:
mem_inst_retired.all_loads
mem_load_retired.l1_hit
mem_load_retired.l1_miss
我期望 all_loads ~= l1_hit + l1_miss
,因为加载指令可能命中或错过 L1 缓存 - 没有其他选择,因为所有常规加载都会经过缓存。我已阅读英特尔文档here据我所知,这并没有说太多,也没有什么可以反驳我的想法。
但是,在运行某些代码时,我注意到总和远低于 all_loads
。
例如,以下程序集将 10 亿个内存位置相加,步长为 32B:
.intel_syntax noprefix
.globl main
main:
sub rsp, 8 # Stack alignment for call
push r12
push r13
mov r12, 0xfffffff # Array size
mov rdi, r12
call malloc # Allocate array
mov r13, rax # Save array pointer
mov rdi, r13 # Write to array to force page-in
mov rsi, 42
mov rdx, r12
call memset
mov rcx, 1000000000 # Loop counter
mov rdx, 0 # Index sequence start
mov rax, 0 # Result accumulator
.p2align 4 # Skylake JCC alignment issue
loop:
mov rdi, rdx
and rdi, r12 # Mask index to array size
movzx rsi, BYTE PTR [r13+rdi] # Read from array index
add rax, rsi
lea rdx, [rdx+32] # Generate next array index
dec rcx # Loop counter & condiiton
jnz loop
pop r13
pop r12
add rsp, 8
ret
它产生以下性能结果:
~$ perf stat -e instructions,cycles,mem_inst_retired.all_loads,mem_load_retired.l1_hit,mem_load_retired.l1_miss ./test
Performance counter stats for './test':
7,000,215,742 instructions:u # 1.25 insn per cycle
5,589,048,737 cycles:u
998,622,922 mem_inst_retired.all_loads:u
17,215,080 mem_load_retired.l1_hit:u
424,118,595 mem_load_retired.l1_miss:u
1.939187022 seconds time elapsed
1.826889000 seconds user
0.112177000 seconds sys
all_loads
正如预期的那样大约为 10 亿(尽管有点低,但可能是一些采样人工制品)。然而,l1_hit + l1_miss
约为 4.5 亿 - 似乎约有 50% 的负载下落不明。
是什么导致 l1_hit
和 l1_miss
的总和不等于 all_loads
?
有趣的是,如果内存加载步幅发生变化,导致几乎所有加载都命中或未命中,则结果往往会趋于 all_loads ~= l1_hit + l1_miss
。只有在中间地带,平等才会被打破。
编辑:我在两个 CPU 上进行了测试:Kaby Lake 和 Ice Lake。两者显示相同的结果。
最佳答案
正如 Margaret Bloom 在评论中指出的那样,加载到相同的缓存行,因为已经未完成的缓存行可以“命中”该 LFB,而不是分配新的缓存行。事实证明,既不计算 l1_hit
也不计算 l1_miss
。它有一个单独的事件 mem_load_retired.fb_hit
。 (l1_miss
只计算导致对 L2 的新请求的指令,而不是同时计算 LFB 命中,这可能是件好事。另请注意,LFB 可能被传出存储占用,包括 NT 存储,因此因此导致的 LFB 命中也被计算在内。可能;这并不总是只是由于多次加载所致。)
您的代码跨度为 32 字节,因此每 64 字节行加载 2 次;第二个通常是 LFB 热门。 (如果硬件预取已经请求,第一个也可能是 LFB 命中,这可能解释了 LFB 命中多于未命中的原因。)
在我的 Skylake i7-6700k 上,使用此测试程序,mem_inst_retired.all_loads
仅比 mem_load_retired.fb_hit + mem_load_retired.l1_hit + mem_load_retired.l1_miss
大 0.6% 左右。
因此,其中的区别仍然有点神秘,即 mem_inst_retired.all_loads
计算的内容,而不是三个更具体的计数器中的任何一个。我预计它们会更接近完全相等,特别是对于 --all-user
或 :u
事件,这样在编程或收集计数器时就不会出现噪音1。
使用 perf stat --no-big-num --all-user -e ...
可以轻松地将数字复制/粘贴到 calc
中,我在一次运行中得到了 hit+miss+LFB = 994.188M 与 all-loads = 999.958M 计数。所以总和低了 0.58%。在重复运行时,这是非常典型的,未命中/命中/LFB 计数器的总和比 mem_inst_retired.all_loads 低一小部分。
再运行几次:
$ perf stat --all-user --no-big-num -e task-clock,page-faults,instructions,cycles,mem_inst_retired.all_loads,mem_load_retired.l1_hit,mem_load_retired.l1_miss,mem_load_retired.fb_hit ./a.out
Performance counter stats for './a.out':
1673.21 msec task-clock # 0.997 CPUs utilized
183 page-faults # 109.371 /sec
7000141672 instructions # 1.56 insn per cycle
4475942186 cycles # 2.675 GHz
999892622 mem_inst_retired.all_loads # 597.590 M/sec
10563966 mem_load_retired.l1_hit # 6.314 M/sec
449822478 mem_load_retired.l1_miss # 268.838 M/sec
533816318 mem_load_retired.fb_hit # 319.038 M/sec
1.677680356 seconds time elapsed
1.647640000 seconds user
0.023197000 seconds sys
$ perf stat --all-user --no-big-num -e task-clock,page-faults,instructions,cycles,mem_inst_retired.all_loads,mem_load_retired.l1_hit,mem_load_retired.l1_miss,mem_load_retired.fb_hit ./a.out
Performance counter stats for './a.out':
1649.17 msec task-clock # 1.000 CPUs utilized
182 page-faults # 110.359 /sec
7000141486 instructions # 1.58 insn per cycle
4419739785 cycles # 2.680 GHz
999850616 mem_inst_retired.all_loads # 606.275 M/sec
9372903 mem_load_retired.l1_hit # 5.683 M/sec
450146459 mem_load_retired.l1_miss # 272.953 M/sec
534244404 mem_load_retired.fb_hit # 323.947 M/sec
1.649504255 seconds time elapsed
1.634275000 seconds user
0.013270000 seconds sys
(我通常在 taskset -c 1
下运行单线程测试,以确保没有 cpu-migration
事件,但在空闲系统上短期运行时通常不会发生这种情况。)
我的 EPP /sys/devices/system/cpu/cpufreq/policy*/energy_performance_preference
) 设置为 balance-performance
(不完整 performance
),因此硬件 P 状态管理在此内存密集型工作负载上的时钟频率降至 2.7GHz。由于 --all-user
,计算出的 2.68GHz 仅计算用户空间周期,但 task-clock
是挂钟时间。 (这在一定程度上减少了每核内存带宽,因为非核心速度减慢,使得延迟 x max_in-flight_lines 成为单核内存带宽的限制因素。这对于本实验来说不是问题,但它是其他不明显但可见的问题在此性能数据中。我的 i7-6700k 有双 channel DDR4-2666,运行 Arch Linux,内核 6.4.9)
脚注 1:即使 --all-user
也并不完美。 @John McCalpin 评论道:
The details vary by implementation, but there are lots and lots of little gotchas when trying to make performance counts accurate in the presence of unnecessary user/kernel crossings. Ice Lake Xeon will undercount if the counters are configured for user-only or kernel-only (errata ICX14). Going through the kernel is OK for coarse measurements (~10%), but for detailed studies of the consistency of different events it is best to avoid leaving user-space.
在让内核对计数器进行编程之后,您可以通过 rdpmc
收集用户空间中的计数来完成此操作。 (也许通过 perf_event_open
。)
可以通过较短的测试间隔来避免该核心上的中断,否则您需要研究 isolcpus=
Linux 内核启动选项,以便您可以测试超过一个时间片的时间,同时仍然避免在定时/期间进行任何用户/内核转换分析区域。
关于caching - 为什么 mem_load_retired.l1_hit 和 mem_load_retired.l1_miss 没有添加到加载总数中?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/77052435/