我一直在尝试找出应用程序中的性能问题,最后将其范围缩小到一个非常奇怪的问题。如果将VZEROUPPER指令注释掉,则以下代码在Skylake CPU(i5-6500)上的运行速度慢6倍。我已经测试了Sandy Bridge和Ivy Bridge CPU,并且无论有没有VZEROUPPER,两个版本都以相同的速度运行。

现在,我对VZEROUPPER的功能有了一个很好的了解,并且当没有VEX编码的指令并且没有对可能包含这些指令的任何函数的调用时,我认为对于此代码根本不重要。它在其他支持AVX的CPU上不存在的事实似乎支持这一点。 Intel® 64 and IA-32 Architectures Optimization Reference Manual中的表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不同,但是显示了相同的问题。较旧版本的intel编译器会生成该函数的另一种变体,该函数也显示了该问题,但前提是main.cpp不是使用intel编译器构建的,因为它会插入调用以初始化其自己的某些库,而这些库可能最终在某处执行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倍,甚至可能更高。

我试过弄乱了程序集,而float指令和double指令一样糟糕。我也无法将问题确定为一条指令。

最佳答案

即使您的整个可见应用程序显然不使用任何AVX指令,您也要因“混合”非VEX SSE和VEX编码的指令而受到惩罚!

在Skylake之前,从使用vex的代码切换到不使用vex的代码时,这种惩罚只是一次过渡惩罚,反之亦然。也就是说,除非您积极混合使用VEX和非VEX,否则您永远不会为过去发生的任何事情支付持续的罚款。但是,在Skylake中,存在一种状态,即即使不进行进一步混合,非VEX SSE指令也会付出很高的持续执行代价。

直接从马口开始,这是图11-1 1-旧的(Skylake之前的)过渡图:

performance - 为什么在Skylake上没有VZEROUPPER的情况下,此SSE代码慢6倍?-LMLPHP

如您所见,所有处罚(红色箭头)都将您带入新的状态,此时不再需要重复执行该操作。例如,如果您通过执行一些256位AVX进入肮脏的上层状态,然后又执行旧版SSE,则需要一次性支付罚款以过渡到保留的非INIT上层状态,但是您无需支付费用在那之后的任何处罚。

在Skylake中,每个都不同,如图11-2 所示:

performance - 为什么在Skylake上没有VZEROUPPER的情况下,此SSE代码慢6倍?-LMLPHP

总体而言,惩罚较少,但对于您的情况而言,关键是其中之一是自我循环:在肮脏的较高状态下执行旧版SSE的惩罚(在图11-2中为惩罚指令)使您始终处于这种状态。州。这就是您所发生的事情-任何AVX指令都会使您进入肮脏的上层状态,这会减慢所有SSE的执行速度。

这是英特尔关于新处罚的规定(第11.3节):



因此,代价显然是非常大的-它必须一直混合最高位以保留它们,而且由于明显地依赖于隐藏的高位,因此还使得显然独立的指令变得依赖。例如xorpd xmm0, xmm0不再打破对xmm0先前值的依赖,因为结果实际上取决于ymm0不会清除的xorpd中隐藏的高位。后者的影响很可能会损害您的性能,因为您现在将拥有很长的依赖链,而通常的分析无法预期。

这是最糟糕的性能陷阱之一:现有体系结构的行为/最佳实践与当前体系结构本质上相反。据推测,硬件架构师有充分的理由进行更改,但是确实在细微的性能问题列表中增加了另一个“陷阱”。

我将针对插入该AVX指令且未跟进VZEROUPPER的编译器或运行时提出错误。

更新:对于以下OP的comment,运行时链接程序ld插入了有问题的(AVX)代码,并且bug已经存在。

1来自Intel的optimization manual

07-27 13:48