如果没有Skylake上的VZEROUPPER,为什么这个SSE代码会慢6倍?

Oli*_*ier 32 performance x86 sse intel avx

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

现在我VZEROUPPER对这个代码有了一个相当好的想法,而且我认为当没有VEX编码指令并且没有调用可能包含它们的任何函数时,它对这个代码根本不重要.事实上它不支持其他支持AVX的CPU似乎支持这一点.英特尔®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;
}
Run Code Online (Sandbox Code Playgroud)

这是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;
}
Run Code Online (Sandbox Code Playgroud)

该函数用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   
Run Code Online (Sandbox Code Playgroud)

生成的代码与gcc不同,但它显示相同的问题.较旧版本的intel编译器生成了另一个函数的变体,它也显示了问题,但只有在main.cpp没有使用intel编译器构建的情况下,因为它插入调用来初始化一些自己的库,这可能最终会在VZEROUPPER某处做.

当然,如果整个东西都是用AVX支持构建的,那么内在函数就会变成VEX编码指令,也没有问题.

我已经尝试perf在linux上分析代码,并且大多数运行时通常依赖于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
Run Code Online (Sandbox Code Playgroud)

好的,正如评论中所怀疑的那样,使用VEX编码指令会导致速度减慢.使用VZEROUPPER清除它.但这仍然无法解释原因.

根据我的理解,不使用VZEROUPPER应该涉及转换到旧的SSE指令的成本,但不是它们的永久性减速.特别是不是那么大的一个.考虑到循环开销,该比率至少为10倍,可能更高.

我试过稍微搞乱程序集,浮动指令和双指令一样糟糕.我无法确定单个指令的问题.

Bee*_*ope 36

您正在经历"混合"非VEX SSE和VEX编码指令的惩罚 - 即使您的整个可见应用程序显然不使用任何AVX指令!

在Skylake之前,当从使用vex的代码切换到没有使用vex的代码时,这种类型的惩罚只是一次性转换惩罚,反之亦然.也就是说,除非您主动混合VEX和非VEX,否则您从未对过去发生的任何事情支付持续罚款.然而,在Skylake,有一种状态,非VEX SSE指令支付高额的持续执行惩罚,即使没有进一步混合.

直接从马的嘴里,这是图11-1 1 - 旧的(前Skylake)过渡图:

前Skylake过渡处罚

正如你所看到的,所有的惩罚(红色箭头)都会带你进入一个新的状态,此时重复这个动作就不再受到惩罚了.例如,如果你通过执行一些256位AVX 进入状态,然后执行传统SSE,你需要支付一次性罚款才能转换到保留的非INIT状态,但你不付钱之后的任何处罚.

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

Skylake处罚

整体惩罚较少,但对于您的情况来说,其中一个是自循环:在状态下执行传统SSE(图11-2中的惩罚A)指令的惩罚使您处于该状态.这就是你所发生的事情 - 任何AVX指令都会让你进入脏的高级状态,这会减慢所有进一步的SSE执行速度.

以下是英特尔关于新处罚的说法(第11.3节):

Skylake微体系结构实现了与前几代不同的状态机,以管理与混合SSE和AVX指令相关的YMM状态转换.在"已修改和未保存"状态下执行SSE指令时,它不再保存整个上YMM状态,而是保存单个寄存器的高位.结果,混合SSE和AVX指令将经历与正在使用的目的地寄存器的部分寄存器依赖性相关联的惩罚以及对目的地寄存器的高位的附加混合操作.

因此惩罚显然​​非常大 - 它必须始终将顶部位混合以保留它们,并且它还使得显然独立地成为依赖的指令,因为存在对隐藏的高位的依赖性.例如,xorpd xmm0, xmm0不再断开对前一个值的依赖xmm0,因为结果实际上取决于隐藏的高位,ymm0而这些高位未被清除xorpd.后一种效应可能会杀死你的表现,因为你现在拥有很长的依赖链,而这些依赖链并不是通常的分析所期望的.

这是最糟糕的性能陷阱之一:先前架构的行为/最佳实践与当前架构基本相反.据推测,硬件架构师有充分的理由进行更改,但它只会在微妙的性能问题列表中添加另一个"问题".

我会针对插入该AVX指令但没有跟进的编译器或运行时提交错误VZEROUPPER.

更新:根据下面的OP 评论,运行时链接程序插入了违规(AVX)代码,ld并且已存在错误.


1来自英特尔的优化手册.

  • 违规代码位于_dl_runtime_resolve_avx(),/ lib64/ld-linux-x86-64.so.2中.看起来像这样应该用glibc的下一个版本排序:https://sourceware.org/bugzilla/show_bug.cgi?id = 20495 (6认同)
  • 有趣的是VZEROUPPER不推荐用于KNL,但情况正在争论中https://software.intel.com/en-us/forums/intel-isa-extensions/topic/704023 (4认同)

A F*_*Fog 16

我刚做了一些实验(在Haswell上).干净状态和脏状态之间的转换并不昂贵,但是脏状态使得每个非VEX向量操作都依赖于目标寄存器的先前值.在您的情况下,例如, movapd %xmm1, %xmm5 将对ymm5具有错误依赖性,这可以防止无序执行.这解释了为什么在AVX代码之后需要vzeroupper.

  • 你是这个网站的[x86]标签的英雄之一.标签的Avid粉丝在此广泛引用您,因为您是x86处理器微架构细节的罕见来源之一.继续你出色的工作! (7认同)
  • @BeeOnRope,OP表示他在Sandy Bridge和Ivy Bridge上没有问题,仅在Skylake上.OP没有测试Haswell.但是Agner看到了Haswell的一个问题.所以我有点困惑,因为在这种情况下,我希望Haswell能像Sandy Bridge和Ivy Bridge一样行事. (3认同)
  • 当VEX和非VEX代码混合时,Haswell的每个状态转换的成本为70个时钟周期,就像Sandy和Ivy Bridge一样.Skylake在状态转换方面没有任何延迟,但我认为它具有与我对Haswell所描述的相同的错误依赖性. (3认同)