首页
学习
活动
专区
圈层
工具
发布
社区首页 >问答首页 >为什么在Skylake上没有VZEROUPPER的情况下SSE代码会慢6倍?

为什么在Skylake上没有VZEROUPPER的情况下SSE代码会慢6倍?
EN

Stack Overflow用户
提问于 2016-12-23 23:09:21
回答 1查看 10.8K关注 0票数 51

我一直在尝试找出应用程序中的一个性能问题,并最终将其缩小到一个非常奇怪的问题。如果注释掉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中有一个bug,它错误地触发了“保存AVX寄存器的上半部分”过程,而这不是它应该做的。

这是main.cpp:

代码语言:javascript
复制
#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:

代码语言:javascript
复制
#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编译为:

代码语言:javascript
复制
 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,克朗,英特尔)。缩短函数似乎会使性能差异逐渐消失,因此看起来是几条指令导致了问题。

编辑:这是一个纯汇编版本,适用于linux。下面的评论。

代码语言:javascript
复制
    .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倍,也许更多。

我尝试过稍微修改一下程序集,浮点指令和双精度指令一样糟糕。我也不能将问题归结于一条指令。

EN

回答 1

Stack Overflow用户

发布于 2016-12-28 17:52:56

我刚刚做了一些实验(在哈斯韦尔上)。干净和脏状态之间的转换并不昂贵,但脏状态使每个非VEX向量操作都依赖于目标寄存器的前一个值。例如,在您的示例中,movapd %xmm1, %xmm5将具有对ymm5的错误依赖,从而防止无序执行。这就解释了为什么在AVX代码之后需要vzeroupper

票数 30
EN
页面原文内容由Stack Overflow提供。腾讯云小微IT领域专用引擎提供翻译支持
原文链接:

https://stackoverflow.com/questions/41303780

复制
相关文章

相似问题

领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档