【问题标题】:Haswell AVX/FMA latencies tested 1 cycle slower than Intel's guide saysHaswell AVX/FMA 延迟测试比英特尔指南慢 1 个周期
【发布时间】:2021-01-14 21:24:31
【问题描述】:

在英特尔内部指南中,vmulpdvfmadd213pd 的延迟为 5,vaddpd 的延迟为 3。

我写了一些测试代码,但是所有的结果都慢了 1 个周期。

这是我的测试代码:

.CODE
test_latency PROC
    vxorpd  ymm0, ymm0, ymm0
    vxorpd  ymm1, ymm1, ymm1

loop_start:
    vmulpd  ymm0, ymm0, ymm1
    vmulpd  ymm0, ymm0, ymm1
    vmulpd  ymm0, ymm0, ymm1
    vmulpd  ymm0, ymm0, ymm1
    sub     rcx, 4
    jg      loop_start

    ret
test_latency ENDP
END
#include <stdio.h>
#include <omp.h>
#include <stdint.h>
#include <windows.h>

extern "C" void test_latency(int64_t n);

int main()
{
    SetThreadAffinityMask(GetCurrentThread(), 1);   // Avoid context switch
    
    int64_t n = (int64_t)3e9;
    double start = omp_get_wtime();
    test_latency(n);
    double end = omp_get_wtime();
    double time = end - start;
    
    double freq = 3.3e9;    // My CPU frequency
    double latency = freq * time / n;
    printf("latency = %f\n", latency);
}

我的 CPU 是 Core i5 4590,我将它的频率锁定在 3.3GHz。输出为:latency = 6.102484

很奇怪,如果我把vmulpd ymm0, ymm0, ymm1改成vmulpd ymm0, ymm0, ymm0,那么输出就变成了:latency = 5.093745

有解释吗?我的测试代码有问题吗?

更多结果

results on Core i5 4590 @3.3GHz
vmulpd  ymm0, ymm0, ymm1       6.056094
vmulpd  ymm0, ymm0, ymm0       5.054515
vaddpd  ymm0, ymm0, ymm1       4.038062
vaddpd  ymm0, ymm0, ymm0       3.029360
vfmadd213pd ymm0, ymm0, ymm1   6.052501
vfmadd213pd ymm0, ymm1, ymm0   6.053163
vfmadd213pd ymm0, ymm1, ymm1   6.055160
vfmadd213pd ymm0, ymm0, ymm0   5.041532

(without vzeroupper)
vmulpd  xmm0, xmm0, xmm1       6.050404
vmulpd  xmm0, xmm0, xmm0       5.042191
vaddpd  xmm0, xmm0, xmm1       4.044518
vaddpd  xmm0, xmm0, xmm0       3.024233
vfmadd213pd xmm0, xmm0, xmm1   6.047219
vfmadd213pd xmm0, xmm1, xmm0   6.046022
vfmadd213pd xmm0, xmm1, xmm1   6.052805
vfmadd213pd xmm0, xmm0, xmm0   5.046843

(with vzeroupper)
vmulpd  xmm0, xmm0, xmm1       5.062350
vmulpd  xmm0, xmm0, xmm0       5.039132
vaddpd  xmm0, xmm0, xmm1       3.019815
vaddpd  xmm0, xmm0, xmm0       3.026791
vfmadd213pd xmm0, xmm0, xmm1   5.043748
vfmadd213pd xmm0, xmm1, xmm0   5.051424
vfmadd213pd xmm0, xmm1, xmm1   5.049090
vfmadd213pd xmm0, xmm0, xmm0   5.051947

(without vzeroupper)
mulpd   xmm0, xmm1             5.047671
mulpd   xmm0, xmm0             5.042176
addpd   xmm0, xmm1             3.019492
addpd   xmm0, xmm0             3.028642

(with vzeroupper)
mulpd   xmm0, xmm1             5.046220
mulpd   xmm0, xmm0             5.057278
addpd   xmm0, xmm1             3.025577
addpd   xmm0, xmm0             3.031238

我的猜测

我把test_latency改成了这样:

.CODE
test_latency PROC
    vxorpd  ymm0, ymm0, ymm0
    vxorpd  ymm1, ymm1, ymm1

loop_start:
    vaddpd  ymm1, ymm1, ymm1  ; added this line
    vmulpd  ymm0, ymm0, ymm1
    vmulpd  ymm0, ymm0, ymm1
    vmulpd  ymm0, ymm0, ymm1
    vmulpd  ymm0, ymm0, ymm1
    sub     rcx, 4
    jg      loop_start

    ret
test_latency ENDP
END

最后我得到了 5 个循环的结果。还有其他指令可以达到同样的效果:

vmovupd     ymm1, ymm0
vmovupd     ymm1, [mem]
vmovdqu     ymm1, [mem]
vxorpd      ymm1, ymm1, ymm1
vpxor       ymm1, ymm1, ymm1
vmulpd      ymm1, ymm1, ymm1
vshufpd     ymm1, ymm1, ymm1, 0

但这些说明不能:

vmovupd     ymm1, ymm2  ; suppose ymm2 is zeroed
vpaddq      ymm1, ymm1, ymm1
vpmulld     ymm1, ymm1, ymm1
vpand       ymm1, ymm1, ymm1

在ymm指令的情况下,我猜避免1个额外循环的条件是:

  1. 所有输入都来自同一个域。
  2. 所有输入都足够新鲜。 (从旧值移动不起作用)

至于 VEX xmm,情况似乎有些模糊。好像和上半部状态有关,但不知道哪个更干净:

vxorpd      ymm1, ymm1, ymm1
vxorpd      xmm1, xmm1, xmm1
vzeroupper

对我来说是个难题。

【问题讨论】:

  • 您进一步的测试都表明,如果您读取寄存器而不写入它,它的“额外延迟”属性可以保留整个循环,通过另一个操作数影响依赖链。 (而且vzeroupper 可以在 Haswell 上清除此属性。它不会在 Skylake 上。)
  • @PeterCordes 其实vzeroupper 只能改变vmulpd xmm0, xmm0, xmm1 的延迟;它对vmulpd ymm0, ymm0, ymm1 没有任何改变。所以我还是很好奇。
  • 有趣。在 Skylake 上,vzeroupper 也没有修复 xmm,如果只读寄存器被污染,仍然很慢。但是Skylake uses a different SSE/AVX transition strategy than Haswell 所以很有可能vzeroupper 有不同的实现细节,这也导致了这种不同。

标签: performance x86-64 intel cpu-architecture avx


【解决方案1】:

自从在 Skylake 上注意到它以来,几年来我一直想写一些关于这个的东西。 https://github.com/travisdowns/uarch-bench/wiki/Intel-Performance-Quirks#after-an-integer-to-fp-bypass-latency-can-be-increased-indefinitely

绕过延迟延迟是“粘性的”:整数 SIMD 指令可以“感染”所有读取该值的未来指令,即使在指令完成后很长时间。我很惊讶“感染”在归零惯用语中幸存下来,尤其是像vxorpd 这样的 FP 归零指令,但我可以在 SKL 上重现这种效果(i7-6700k,在测试循环中直接计算时钟周期,perf on Linux 而不是搞乱时间和频率。)

(在 Skylake 上,似乎有 3 个或更多 vxorpd 在循环发生之前连续清零指令,消除了额外的旁路延迟。AFAIK,xor-zeroing 总是被消除,不像mov-elimination 有时会失败。但也许区别只是在将vpaddb 发布到后端和第一个vmulpd 之间造成差距;在我的测试循环中,我之前“脏”/污染了寄存器循环。)

更新:现在再次尝试我的测试代码,即使是vxorps 似乎也清理了寄存器。也许微码更新改变了一些东西。)

大概之前在调用者中对 YMM1 的一些使用涉及整数指令。 (TODO:调查寄存器进入这种状态的常见程度,以及它何时可以在异或归零中幸存下来!我希望它只会在使用整数指令构造 FP 位模式时发生,包括像 vpcmpeqd ymm1,ymm1,ymm1 这样的东西制作一个 -NaN(全一位)。)

在 Skylake 上,我可以通过在 xor-zeroing 之后执行 vaddpd ymm1, ymm1, ymm1 before 循环来修复它。 (或者之前;可能没关系!这可能更优化,将它放在前一个 dep 链的末尾而不是这个的开头。)


正如我写的in a comment on another question

xsave/rstor 可以解决写寄存器的问题 像 paddd 这样的 SIMD 整数指令会无限期地产生额外的延迟 使用 FP 指令读取它,影响两者的延迟 输入。例如paddd xmm0, xmm0 然后在循环中 addps xmm1, xmm0 有 5c 延迟而不是通常的 4,直到下一次保存/恢复。

这是 绕过延迟,但即使您不触摸寄存器仍然会发生 直到 paddd 确实退休之后(通过使用 >ROB 填充 uops) 在循环之前。


测试程序:

; taskset -c 3 perf stat --all-user -etask-clock,context-switches,cpu-migrations,page-faults,cycles,branches,instructions,uops_issued.any,uops_executed.thread -r1 ./bypass-latency

default rel
global _start
_start:
    vmovaps   xmm1, [one]        ; FP load into ymm1 (zeroing the upper lane)
    vpaddd    ymm1, ymm1,ymm0   ; ymm1 written in the ivec domain
    ;vxorps    ymm1, ymm1,ymm1   ; In 2017, ymm1 still makes vaddps slow (5c) after this
    ; but I can't reproduce that now with updated microcode.
    vxorps    ymm0, ymm0, ymm0   ; zeroing-idiom on ymm0
    mov       rcx, 50000000

align 32  ; doesn't help or hurt, as expected since the bottleneck isn't frontend
.loop:
    vaddps  ymm0, ymm0,ymm1
    vaddps  ymm0, ymm0,ymm1
    dec     rcx
    jnz .loop

    xor edi,edi
    mov eax,231
    syscall      ; exit_group(0)

section .rodata
align 16
one:            times 4 dd 1.0

Perf 在 i7-6700k 上生成静态可执行文件:

 Performance counter stats for './foo' (4 runs):

            129.01 msec task-clock                #    0.998 CPUs utilized            ( +-  0.51% )
                 0      context-switches          #    0.000 K/sec                  
                 0      cpu-migrations            #    0.000 K/sec                  
                 2      page-faults               #    0.016 K/sec                  
       500,053,798      cycles                    #    3.876 GHz                      ( +-  0.00% )
        50,000,042      branches                  #  387.576 M/sec                    ( +-  0.00% )
       200,000,059      instructions              #    0.40  insn per cycle           ( +-  0.00% )
       150,020,084      uops_issued.any           # 1162.883 M/sec                    ( +-  0.00% )
       150,014,866      uops_executed.thread      # 1162.842 M/sec                    ( +-  0.00% )

          0.129244 +- 0.000670 seconds time elapsed  ( +-  0.52% )

500M 循环用于 50M 迭代 = 2x vaddps 的 10 循环循环携带依赖,或每个 5。

【讨论】:

  • 我尝试在vxorpd之前或之后添加vaddpd ymm1, ymm1, ymm1,但vmulpd ymm0, ymm0, ymm1的延迟仍然是6。
  • @kevinjwz:很遗憾,我没有可以测试的可用 Haswell 系统,但我可以在 Skylake 上重现它。 vpaddb ymm1, ymm1, ymm1 在循环“感染”寄存器之前,使其变慢。 vaddpd ymm1, ymm1, ymm1 之后又让它变得更快(每个vmulpd 4 个周期;Skylake 的 mul/add/FMA 有 4c 延迟,放弃了 Haswell 拥有的 3c 延迟专用 FP 添加单元)。而且我可以确认vxorpd-zeroing 在vpaddb 之后 清理寄存器! (不过,FP shuffle 确实如此,例如 vunpcklpd。或者 3 次或更多次异或归零重复。非常神秘。)
  • re:“在 Skylake 上,似乎有 3 个或更多 vxorpd 归零指令在循环发生之前连续执行,消除了额外的旁路延迟”您是否使用 1x vxorpd + nop 填充进行了测试看看它是否真的只是分离解码组?
  • @Noah:不,我还没有。你能在你的威士忌湖机器上重现这种效果吗? (和/或冰湖?)
  • 你能把基准代码贴在某个地方,我可以试试。
猜你喜欢
  • 2016-06-21
  • 2016-06-08
  • 2013-03-17
  • 2011-05-10
  • 2015-12-15
  • 2020-10-02
  • 1970-01-01
  • 2020-03-21
相关资源
最近更新 更多