Performance 英特尔Skylake上存储循环的性能出人意料地糟糕且怪异

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 {

我发现一个简单的存储循环的性能出人意料地差,它有两个存储:一个向前跨步为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 {
        *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的对齐,没有任何更改。默认情况下也会消除
  • 与机器上其他进程的争用:在空闲机器上,甚至在重载机器上(例如,使用
    stress-vm4
    )观察到的效果或多或少相同。基准本身无论如何都应该是完全核心本地的,因为它适合L2,并且
    perf
    确认每个迭代很少有L2未命中(大约每300-400次迭代有1次未命中,可能与
    printf
    代码有关)
  • 涡轮增压器:涡轮增压器完全禁用,由三个不同的MHz读数确认
  • 省电功能:性能调控器在
    性能
    模式下运行。测试期间未观察到频率变化(CPU基本上保持在2.59 GHz的锁定状态)
  • TLB效果:即使输出缓冲区位于2MB的大页面中,效果仍然存在。在任何情况下,64个4k TLB条目都超过了128K输出缓冲区
    perf
    不报告任何特别奇怪的TLB行为
  • 4k别名:此基准测试的较旧、更复杂版本确实显示了一些4k别名,但由于基准测试中没有加载(加载可能会错误地对早期存储进行别名),因此已消除了这种情况。默认情况下也会消除
  • L2关联性冲突:通过默认消除消除,并且由于即使使用2MB页面也不会消除这种冲突,因此我们可以确保输出缓冲区在物理内存中呈线性排列
  • 超读效应:HT被禁用
  • 预取:此处只能涉及两个预取器(“DCU”,也称为L1L2预取器),因为所有数据都存在于L1或L2中,但在启用或禁用所有预取器的情况下,性能相同
  • 中断:中断计数和慢模式之间没有相关性。总中断次数有限,主要是时钟滴答声
托普列夫 我使用的是实现Intel分析方法的,它将基准识别为存储绑定,这一点也不奇怪:

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