为什么Skylake处理器上没有使用VZEROUPPER指令会使得这个SSE代码运行速度慢6倍?

63

我一直在尝试解决一个应用程序的性能问题,并最终将其缩小到了一个非常奇怪的问题。如果注释掉 VZEROUPPER 指令,下面的代码块在 Skylake CPU(i5-6500)上运行速度会慢 6 倍。我已经测试过 Sandy Bridge 和 Ivy Bridge CPU,两个版本都可以在相同的速度下运行,无论是否使用 VZEROUPPER

现在我对 VZEROUPPER 的作用有了相当好的了解,并且认为当没有 VEX 编码指令和任何可能包含这些指令的函数调用时,它对于这段代码来说根本不重要。事实上,其他 AVX 能力的 CPU 上也是如此。 Intel® 64 和 IA-32 架构优化参考手册 中的表 11-2 也支持这一点。

那么出了什么问题呢?

我唯一剩下的理论是 CPU 中存在一个错误,并在不该触发“保存 AVX 寄存器的上半部分”过程时错误地触发了它。或者其他一些同样奇怪的问题。

这是 main.cpp:

#include <immintrin.h>

int slow_function( double i_a, double i_b, double i_c );

int main()
{
    /* DAZ and FTZ, does not change anything here. */
    _mm_setcsr( _mm_getcsr() | 0x8040 );

    /* This instruction fixes performance. */
    __asm__ __volatile__ ( "vzeroupper" : : : );

    int r = 0;
    for( unsigned j = 0; j < 100000000; ++j )
    {
        r |= slow_function( 
                0.84445079384884236262,
                -6.1000481519580951328,
                5.0302160279288017364 );
    }
    return r;
}

这是slow_function.cpp:

#include <immintrin.h>

int slow_function( double i_a, double i_b, double i_c )
{
    __m128d sign_bit = _mm_set_sd( -0.0 );
    __m128d q_a = _mm_set_sd( i_a );
    __m128d q_b = _mm_set_sd( i_b );
    __m128d q_c = _mm_set_sd( i_c );

    int vmask;
    const __m128d zero = _mm_setzero_pd();

    __m128d q_abc = _mm_add_sd( _mm_add_sd( q_a, q_b ), q_c );

    if( _mm_comigt_sd( q_c, zero ) && _mm_comigt_sd( q_abc, zero )  )
    {
        return 7;
    }

    __m128d discr = _mm_sub_sd(
        _mm_mul_sd( q_b, q_b ),
        _mm_mul_sd( _mm_mul_sd( q_a, q_c ), _mm_set_sd( 4.0 ) ) );

    __m128d sqrt_discr = _mm_sqrt_sd( discr, discr );
    __m128d q = sqrt_discr;
    __m128d v = _mm_div_pd(
        _mm_shuffle_pd( q, q_c, _MM_SHUFFLE2( 0, 0 ) ),
        _mm_shuffle_pd( q_a, q, _MM_SHUFFLE2( 0, 0 ) ) );
    vmask = _mm_movemask_pd(
        _mm_and_pd(
            _mm_cmplt_pd( zero, v ),
            _mm_cmple_pd( v, _mm_set1_pd( 1.0 ) ) ) );

    return vmask + 1;
}

使用clang编译后,该函数的编译结果如下:

 0:   f3 0f 7e e2             movq   %xmm2,%xmm4
 4:   66 0f 57 db             xorpd  %xmm3,%xmm3
 8:   66 0f 2f e3             comisd %xmm3,%xmm4
 c:   76 17                   jbe    25 <_Z13slow_functionddd+0x25>
 e:   66 0f 28 e9             movapd %xmm1,%xmm5
12:   f2 0f 58 e8             addsd  %xmm0,%xmm5
16:   f2 0f 58 ea             addsd  %xmm2,%xmm5
1a:   66 0f 2f eb             comisd %xmm3,%xmm5
1e:   b8 07 00 00 00          mov    $0x7,%eax
23:   77 48                   ja     6d <_Z13slow_functionddd+0x6d>
25:   f2 0f 59 c9             mulsd  %xmm1,%xmm1
29:   66 0f 28 e8             movapd %xmm0,%xmm5
2d:   f2 0f 59 2d 00 00 00    mulsd  0x0(%rip),%xmm5        # 35 <_Z13slow_functionddd+0x35>
34:   00 
35:   f2 0f 59 ea             mulsd  %xmm2,%xmm5
39:   f2 0f 58 e9             addsd  %xmm1,%xmm5
3d:   f3 0f 7e cd             movq   %xmm5,%xmm1
41:   f2 0f 51 c9             sqrtsd %xmm1,%xmm1
45:   f3 0f 7e c9             movq   %xmm1,%xmm1
49:   66 0f 14 c1             unpcklpd %xmm1,%xmm0
4d:   66 0f 14 cc             unpcklpd %xmm4,%xmm1
51:   66 0f 5e c8             divpd  %xmm0,%xmm1
55:   66 0f c2 d9 01          cmpltpd %xmm1,%xmm3
5a:   66 0f c2 0d 00 00 00    cmplepd 0x0(%rip),%xmm1        # 63 <_Z13slow_functionddd+0x63>
61:   00 02 
63:   66 0f 54 cb             andpd  %xmm3,%xmm1
67:   66 0f 50 c1             movmskpd %xmm1,%eax
6b:   ff c0                   inc    %eax
6d:   c3                      retq   

生成的代码与gcc不同,但显示了相同的问题。旧版本的英特尔编译器生成了另一种函数变体,也显示了该问题,但仅当 main.cpp 未使用英特尔编译器构建时,它插入调用以初始化其自己的某些库,这可能会在某个地方执行 VZEROUPPER

当然,如果整个东西都支持AVX,因此将内部处理转换为VEX编码指令,那么也没有问题。

我已尝试使用linux上的 perf 对代码进行分析,大多数运行时通常落在1-2条指令上,但取决于我分析的代码版本(gcc、clang、intel),并非总是相同的指令。缩短函数似乎会逐渐消除性能差异,因此看起来有几条指令造成了问题。

编辑:这是一个纯汇编版本,适用于linux。请见下文。

    .text
    .p2align    4, 0x90
    .globl _start
_start:

    #vmovaps %ymm0, %ymm1  # This makes SSE code crawl.
    #vzeroupper            # This makes it fast again.

    movl    $100000000, %ebp
    .p2align    4, 0x90
.LBB0_1:
    xorpd   %xmm0, %xmm0
    xorpd   %xmm1, %xmm1
    xorpd   %xmm2, %xmm2

    movq    %xmm2, %xmm4
    xorpd   %xmm3, %xmm3
    movapd  %xmm1, %xmm5
    addsd   %xmm0, %xmm5
    addsd   %xmm2, %xmm5
    mulsd   %xmm1, %xmm1
    movapd  %xmm0, %xmm5
    mulsd   %xmm2, %xmm5
    addsd   %xmm1, %xmm5
    movq    %xmm5, %xmm1
    sqrtsd  %xmm1, %xmm1
    movq    %xmm1, %xmm1
    unpcklpd    %xmm1, %xmm0
    unpcklpd    %xmm4, %xmm1

    decl    %ebp
    jne    .LBB0_1

    mov $0x1, %eax
    int $0x80

好的,正如评论中所怀疑的那样,使用VEX编码指令会导致减速。使用VZEROUPPER可以解决这个问题。但这仍然无法解释为什么会出现这种情况。

据我理解,不使用VZEROUPPER应该涉及到转换为旧SSE指令的成本,但不会对它们造成永久性的减速。尤其不是如此大的减速。考虑到循环开销,比率至少是10倍,也许更高。

我已经尝试过对汇编进行一些调整,浮点指令和双精度指令一样糟糕。我也无法将问题定位到单个指令。


1
你使用了哪些编译器标志?也许(隐藏的)进程初始化正在使用一些 VEX 指令,这会使你处于一个混合状态,从中你永远无法退出。你可以尝试复制/粘贴汇编代码,并构建一个带有“_start”的纯汇编程序,以避免任何编译器插入的初始化代码,并查看是否出现相同的问题。 - BeeOnRope
1
我终于坐下来看了文档。在英特尔的手册中,惩罚已经被讨论得非常清楚了,虽然 Skylake 的情况有所不同,但并不一定更好 - 在你的情况下,它会更糟。我在答案中添加了详细信息。 - BeeOnRope
2
@Zboson AVX指令在动态链接器中,但我也不知道为什么他们把它放在那里。请参见我对BeeOnRope答案的评论。这是一个相当棘手的问题。 - Olivier
@BeeOnRope 我现在明白了,这回答了我的下一个问题。假设 OP 使用这个汇编代码在其他系统上进行测试,因为 SNB 和 IVB 系统可能没有相同的 /lib64/ld-linux-x86-64.so.2 - Z boson
1
@Zboson我认为在某个点上我的测试用例很慢,在测试循环之前的main()中有一个printf(),而没有则很快。 我用stepi在gdb中跟踪,并快速进入那个充满avx代码而没有vzeroupper的函数。 几次搜索后,我找到了glibc问题,它明确表示存在问题。 我后来发现memset()同样存在问题,但不知道原因(代码看起来没问题)。 - Olivier
显示剩余13条评论
2个回答

79
您正在经历“混合”非VEX SSE和VEX编码指令的惩罚-即使您的整个可见应用程序显然没有使用任何AVX指令!在Skylake之前,这种类型的惩罚只是一次“转换”惩罚,当从使用vex的代码切换到不使用vex的代码或反之亦然时。也就是说,除非您积极地混合VEX和非VEX,否则您从未为过去发生的事情付出持续的惩罚。然而,在Skylake中,有一种状态,其中非VEX SSE指令支付高额的持续执行惩罚,即使没有进一步的混合。直接从权威消息来源传来,这是旧的(Skylake之前的)转换图11-1 -:

Pre-Skylake Transition Penalties

作为您的助手,我可以翻译以下内容:

正如您所看到的,所有的惩罚(红色箭头)都会使您进入一个新的状态,在此之后重复该操作将不再受到惩罚。例如,如果您通过执行某些256位AVX来达到dirty upper状态,然后执行传统SSE,您需要支付一次性罚款以过渡到preserved non-INIT upper状态,但在此之后您就不需要支付任何罚款了。

根据图11-2,在Skylake中,一切都是不同的:

Skylake Penalties

在总体上,惩罚较少,但对于您的情况至关重要的是,其中一个是自环:在“脏上”状态下执行遗留SSE(图11-2中的惩罚A)指令的惩罚将使您保持在该状态。这就是发生在您身上的事情 - 任何AVX指令都会使您处于“脏上”状态,从而减缓所有后续SSE执行速度。
以下是英特尔关于新惩罚的说法(第11.3节):
Skylake微体系结构实现了不同的状态机来管理与混合SSE和AVX指令相关的YMM状态转换。当处于“修改和未保存”状态时,它不再保存整个上部YMM状态,而是保存每个寄存器的上位比特。因此,混合SSE和AVX指令将经历与正在使用目标寄存器的部分寄存器依赖性相关的惩罚,并进行目标寄存器的上位比特的额外混合操作。
所以处罚显然非常严厉 - 它必须始终混合顶部位以保留它们,并且还会使明显独立的指令变为依赖项,因为存在对隐藏的上位位的依赖。例如,xorpd xmm0,xmm0不再打破先前值的依赖关系xmm0,因为结果实际上依赖于ymm0中的隐藏的上位位,这些位不被xorpd清除。后一种影响可能会导致性能下降,因为您现在将拥有非常长的依赖链,而这是从通常的分析中不会预期的。
这是最糟糕的性能陷阱之一:先前架构的行为/最佳实践基本上与当前架构相反。硬件架构师很可能有充分的理由进行更改,但它确实只是在微妙的性能问题列表中增加了另一个“陷阱”。
我会针对插入该AVX指令并未跟进VZEROUPPER的编译器或运行时文件提交错误报告。 更新: 根据OP在评论中的说明,有害代码(AVX)是由运行时链接器ld插入的,已经存在漏洞

1来自英特尔的优化手册


1
太好了!我一开始读的是没有Skylake注释的旧版手册,然后又读了新版但还不够完整。更糟糕的是,新版比旧版页数还少。我一定会找到那个有问题的库。 - Olivier
8
有问题的代码位于/lib64/ld-linux-x86-64.so.2中的_dl_runtime_resolve_avx()函数。看起来这个问题应该会在下一个glibc版本中得到解决:https://sourceware.org/bugzilla/show_bug.cgi?id=20495 - Olivier
4
有趣的是,VZEROUPPER在KNL上并不被推荐使用,但这种情况正在进行讨论。https://software.intel.com/en-us/forums/intel-isa-extensions/topic/704023 - Z boson
1
为什么除非编译了带有 AVX 的 main.cpp,否则 OP 在 slow_function.cpp 中不会得到 AVX 指令?GCC 不应该插入 AVX 指令,除非它被告知这样做,因为在没有 AVX 的系统上会生成 SIGILL - Z boson
@MaximMasiutin,你的代码格式有问题,所以我无法解析它。但简单来说,问题不在于混合使用ymmxmm寄存器,而是关于混合使用非VEX和VEX编码指令。任何带有ymm的都是VEX编码,几乎所有带有3个参数的都是VEX编码,我认为所有以v开头的向量指令都是VEX编码。因此,在你的示例中,如果你将其更改为使用vmovdqu xmm,...,那么应该就可以了,因为这种形式是VEX编码的。 - BeeOnRope
显示剩余5条评论

37

我刚做了一些实验(在一个Haswell上)。从干净状态到脏状态的转换不昂贵,但脏状态会使得每个非VEX向量操作都依赖于目标寄存器先前的值。在您的情况下,例如 movapd %xmm1, %xmm5 将对 ymm5 产生虚假依赖,这会阻止乱序执行。这就解释了为什么AVX代码之后需要使用 vzeroupper


14
你是这个网站 [x86] 标签下的英雄之一。该标签的狂热追随者经常引用你的内容,因为你是讲解 x86 处理器微体系结构细节的稀有资料来源之一。请继续保持下去! - Iwillnotexist Idonotexist
3
楼主说他在Sandy Bridge和Ivy Bridge上没有遇到这个问题,只有在Skylake上有。楼主没有测试过Haswell。但是Agner在Haswell上发现了一个问题。所以我有点困惑,因为我本来期望Haswell在这种情况下会像Sandy Bridge和Ivy Bridge一样表现。 - Z boson
1
Haswell是否可能实际上表现得像Skylake,但在SKL出现之前没有人描述过这种行为?或者它有时会以这种方式表现?在256b执行单元的上半部分上电之前的热身期间,这只是一个因素吗?也许在AVX-256指令变慢的时期,状态转换行为是不同的?我刚刚买了一台SKL桌面电脑,并且我可以使用一台Haswell笔记本电脑,所以我可能会找时间测试一下。不幸的是,我无法与IvB或SnB进行比较,我认为它们确实按照您和英特尔所描述的方式工作。 - Peter Cordes
3
Peter,Haswell处理器在混合使用VEX和非VEX代码时,每个状态转换需要70个时钟周期的成本,就像Sandy和Ivy Bridge一样。Skylake处理器在状态转换上没有延迟,但我认为它有与Haswell类似的误依赖问题。 - A Fog
1
只是一个有趣的事实(现在要睡觉了,只是在挖掘,如果有人关心,请联系我) - 看起来Skylake是否使用微码补丁来禁用循环流解码器也会产生差异(不知何故)- 你无法想象找出原因有多痛苦,但现在我可以可靠地得到结果,所以...就是这样。 - Alec Teal
显示剩余5条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接