Performance 英特尔Skylake上存储循环的性能出人意料地糟糕且怪异
我发现一个简单的存储循环的性能出人意料地差,它有两个存储:一个向前跨步为16字节,另一个总是在同一个位置1,如下所示:Performance 英特尔Skylake上存储循环的性能出人意料地糟糕且怪异,performance,assembly,optimization,x86,x86-64,Performance,Assembly,Optimization,X86,X86 64,我发现一个简单的存储循环的性能出人意料地差,它有两个存储:一个向前跨步为16字节,另一个总是在同一个位置1,如下所示: volatile uint32_t value; void weirdo_cpp(size_t iters, uint32_t* output) { uint32_t x = value; uint32_t *rdx = output; volatile uint32_t *rsi = output; do {
volatile uint32_t value;
void weirdo_cpp(size_t iters, uint32_t* output) {
uint32_t x = value;
uint32_t *rdx = output;
volatile uint32_t *rsi = output;
do {
*rdx = x;
*rsi = x;
rdx += 4; // 16 byte stride
} while (--iters > 0);
}
在汇编中,此循环可能看起来像:
weirdo_cpp:
...
align 16
.top:
mov [rdx], eax ; stride 16
mov [rsi], eax ; never changes
add rdx, 16
dec rdi
jne .top
ret
当访问的内存区域在L2中时,我希望每次迭代的运行周期少于3个。第二家商店一直在同一地点,应该会增加大约一个周期。第一个存储意味着从L2引入一行,因此每4次迭代也会逐出一行。我不确定您如何评估L2成本,但即使您保守地估计L1在每个周期只能执行以下操作之一:(a)提交一个存储或(b)从L2接收一行或(c)从L2逐出一行到L2,对于Strip-16存储流,您也会得到类似于1+0.25+0.25=1.5个周期的结果
事实上,如果对一个存储进行注释,则仅第一个存储每次迭代得到约1.25个周期,第二个存储每次迭代得到约1.01个周期,因此每次迭代2.5个周期似乎是保守估计
然而,实际表现非常奇怪。以下是测试线束的典型运行:
Estimated CPU speed: 2.60 GHz
output size : 64 KiB
output alignment: 32
3.90 cycles/iter, 1.50 ns/iter, cpu before: 0, cpu after: 0
3.90 cycles/iter, 1.50 ns/iter, cpu before: 0, cpu after: 0
3.90 cycles/iter, 1.50 ns/iter, cpu before: 0, cpu after: 0
3.89 cycles/iter, 1.49 ns/iter, cpu before: 0, cpu after: 0
3.90 cycles/iter, 1.50 ns/iter, cpu before: 0, cpu after: 0
4.73 cycles/iter, 1.81 ns/iter, cpu before: 0, cpu after: 0
7.33 cycles/iter, 2.81 ns/iter, cpu before: 0, cpu after: 0
7.33 cycles/iter, 2.81 ns/iter, cpu before: 0, cpu after: 0
7.34 cycles/iter, 2.81 ns/iter, cpu before: 0, cpu after: 0
7.26 cycles/iter, 2.80 ns/iter, cpu before: 0, cpu after: 0
7.28 cycles/iter, 2.80 ns/iter, cpu before: 0, cpu after: 0
7.31 cycles/iter, 2.81 ns/iter, cpu before: 0, cpu after: 0
7.29 cycles/iter, 2.81 ns/iter, cpu before: 0, cpu after: 0
7.28 cycles/iter, 2.80 ns/iter, cpu before: 0, cpu after: 0
7.29 cycles/iter, 2.80 ns/iter, cpu before: 0, cpu after: 0
7.27 cycles/iter, 2.80 ns/iter, cpu before: 0, cpu after: 0
7.30 cycles/iter, 2.81 ns/iter, cpu before: 0, cpu after: 0
7.30 cycles/iter, 2.81 ns/iter, cpu before: 0, cpu after: 0
7.28 cycles/iter, 2.80 ns/iter, cpu before: 0, cpu after: 0
7.28 cycles/iter, 2.80 ns/iter, cpu before: 0, cpu after: 0
这里有两件事很奇怪
首先是双峰计时:有快速模式和慢速模式。我们以慢模式开始,每次迭代大约需要7.3个周期,然后在某个点上转换到每次迭代大约3.9个周期。这种行为是一致的和可重复的,并且围绕这两个值聚集的两个计时总是非常一致的。从慢模式到快模式的转换显示在两个方向上,反之亦然(有时一次运行中会出现多个转换)
另一件奇怪的事是,表演真的很糟糕。即使在快速模式下,在大约3.9个周期时,性能也比将每个案例与单个存储相加时所期望的1.0+1.3=2.3个周期的最差情况差得多(并且假设当两个存储都在循环中时,绝对零工作可以重叠)。在慢速模式下,性能与基于第一原则的预期相比非常糟糕:执行2个存储需要7.3个周期,如果将其放在二级存储带宽中,则每个二级存储大约需要29个周期(因为我们每4次迭代只存储一个完整的缓存线)
Skylake在L1和L2之间的吞吐量为64B/周期,远高于此处观察到的吞吐量(慢速模式下约为2字节/周期)
吞吐量和双模性能差的原因是什么?我可以避免吗?
我也很好奇,这是否会复制到其他架构上,甚至在其他Skylake盒子上。欢迎在评论中加入本地结果
你可以找到那个。对于Linux或类似Unix的平台,有一个Makefile
,但在Windows上构建它也应该相对容易。如果您想运行<代码> ASM< /Case>变体,您需要使用汇编> NASM <代码>或<代码> YASM<代码>,如果您没有,可以尝试C++版本。
排除可能性
以下是我考虑过的一些可能性,基本上排除了这些可能性。许多可能的可能性被简单的事实所消除,即在基准测试循环的中间,随机地看到性能转移,当许多事情根本没有改变(例如,如果它与输出阵列对齐有关,它不能在运行的中间改变,因为整个时间使用相同的缓冲器)。我将在下面将其称为默认消除(即使对于默认消除,通常也会有另一个论点)
- 对齐因子:输出数组是16字节对齐的,我已经尝试了2MB的对齐,没有任何更改。默认情况下也会消除
- 与机器上其他进程的争用:在空闲机器上,甚至在重载机器上(例如,使用
)观察到的效果或多或少相同。基准本身无论如何都应该是完全核心本地的,因为它适合L2,并且stress-vm4
确认每个迭代很少有L2未命中(大约每300-400次迭代有1次未命中,可能与perf
代码有关)printf
- 涡轮增压器:涡轮增压器完全禁用,由三个不同的MHz读数确认
- 省电功能:性能调控器在
模式下运行。测试期间未观察到频率变化(CPU基本上保持在2.59 GHz的锁定状态)性能
- TLB效果:即使输出缓冲区位于2MB的大页面中,效果仍然存在。在任何情况下,64个4k TLB条目都超过了128K输出缓冲区
不报告任何特别奇怪的TLB行为perf
- 4k别名:此基准测试的较旧、更复杂版本确实显示了一些4k别名,但由于基准测试中没有加载(加载可能会错误地对早期存储进行别名),因此已消除了这种情况。默认情况下也会消除
- L2关联性冲突:通过默认消除消除,并且由于即使使用2MB页面也不会消除这种冲突,因此我们可以确保输出缓冲区在物理内存中呈线性排列
- 超读效应:HT被禁用李>
- 预取:此处只能涉及两个预取器(“DCU”,也称为L1L2预取器),因为所有数据都存在于L1或L2中,但在启用或禁用所有预取器的情况下,性能相同
- 中断:中断计数和慢模式之间没有相关性。总中断次数有限,主要是时钟滴答声
BE Backend_Bound: 82.11 % Slots [ 4.83%]
BE/Mem Backend_Bound.Memory_Bound: 59.64 % Slots [ 4.83%]
BE/Core Backend_Bound.Core_Bound: 22.47 % Slots [ 4.83%]
BE/Mem Backend_Bound.Memory_Bound.L1_Bound: 0.03 % Stalls [ 4.92%]
This metric estimates how often the CPU was stalled without
loads missing the L1 data cache...
Sampling events: mem_load_retired.l1_hit:pp mem_load_retired.fb_hit:pp
BE/Mem Backend_Bound.Memory_Bound.Store_Bound: 74.91 % Stalls [ 4.96%] <==
This metric estimates how often CPU was stalled due to
store memory accesses...
Sampling events: mem_inst_retired.all_stores:pp
BE/Core Backend_Bound.Core_Bound.Ports_Utilization: 28.20 % Clocks [ 4.93%]
BE/Core Backend_Bound.Core_Bound.Ports_Utilization.1_Port_Utilized: 26.28 % CoreClocks [ 4.83%]
This metric represents Core cycles fraction where the CPU
executed total of 1 uop per cycle on all execution ports...
MUX: 4.65 %
PerfMon Event Multiplexing accuracy indicator
BE Backend\u-Bound:
top:
mov BYTE PTR [rdx],al
add rdx,0x40
sub rdi,0x1
jne top
top:
mov BYTE PTR [rdx],al
mov BYTE PTR [rdx+0x1],al
add rdx,0x40
sub rdi,0x1
jne top
count cycles/itr
1 3.0
51 3.1
5 3.2
5 3.3
12 3.4
733 3.5
139 3.6
22 3.7
2 3.8
11 4.0
16 4.1
1 4.3
2 4.4
top:
mov BYTE PTR [rdx + FIRST],al
mov BYTE PTR [rdx + SECOND],al
add rdx,0x40
sub rdi,0x1
jne top