为啥在 Skylake 上没有 VZEROUPPER 时,这个 SSE 代码会慢 6 倍?

Posted

技术标签:

【中文标题】为啥在 Skylake 上没有 VZEROUPPER 时,这个 SSE 代码会慢 6 倍?【英文标题】:Why is this SSE code 6 times slower without VZEROUPPER on Skylake?为什么在 Skylake 上没有 VZEROUPPER 时,这个 SSE 代码会慢 6 倍? 【发布时间】:2016-12-23 15:09:20 【问题描述】:

我一直试图找出应用程序中的性能问题,最后将其缩小为一个非常奇怪的问题。如果 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 编译器构建的,因为它插入调用以初始化其自己的一些库,这些库可能最终会执行 @987654330 @某处。

当然,如果整个东西都是在支持 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

好的,正如 cmets 中所怀疑的那样,使用 VEX 编码指令会导致速度变慢。使用VZEROUPPER 可以清除它。但这仍然不能解释原因。

据我了解,不使用 VZEROUPPER 应该会涉及转换到旧 SSE 指令的成本,但不会导致它们的永久减速。尤其是没有这么大的。考虑到循环开销,该比率至少是 10 倍,甚至可能更多。

我尝试过稍微弄乱程序集,浮点指令和双指令一样糟糕。我也无法将问题定位到单个指令。

【问题讨论】:

您使用什么编译器标志?也许(隐藏的)进程初始化正在使用一些 VEX 指令,这使您处于永远不会退出的混合状态。您可以尝试复制/粘贴程序集并使用_start 将其构建为纯程序集程序,这样您就可以避免任何编译器插入的初始化代码并查看它是否存在相同的问题。 @BeeOnRope 我使用-O3 -ffast-math,但即使使用-O0,效果也存在。我会尝试纯组装。我刚刚在Agner's blog 上发现,您可能正在关注 VEX 转换的处理方式发生了一些重大的内部变化......需要研究一下。 我终于下车阅读文档。英特尔的手册中非常清楚地讨论了惩罚,虽然对 Skylake 不同,它没有必要更好 - 而在你的情况下它更糟。我在答案中添加了详细信息。 @Zboson AVX 指令在动态链接器中,但我也不知道他们为什么把它放在那里。请参阅我对 BeeOnRope 答案的评论。这是一个相当丑陋的问题。 @Zboson 我认为在某些时候我的测试用例在测试循环之前在main() 中的printf() 很慢,而在没有测试循环的情况下很快。我使用 stepi 在 gdb 中进行了跟踪,并迅速进入了充满 avx 代码且没有 vzeroupper 的函数。几次搜索后,我发现了 glibc 问题,它清楚地表明那里存在问题。从那以后我发现memset() 同样有问题,但不知道为什么(代码看起来不错)。 【参考方案1】:

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

在 Skylake 之前,当从使用 vex 的代码切换到不使用 vex 的代码时,这种类型的惩罚只是一次性的transition 惩罚,反之亦然。也就是说,除非你积极混合 VEX 和非 VEX,否则你永远不会为过去发生的任何事情付出持续的惩罚。然而,在 Skylake 中,即使没有进一步混合,非 VEX SSE 指令也会付出高昂的持续执行代价。

直截了当,这是图11-1 1 - 旧的(Skylake之前)过渡图:

如您所见,所有的惩罚(红色箭头)都会将您带到一个新的状态,此时重复该动作不再受到惩罚。例如,如果您通过执行一些 256 位 AVX 进入 dirty upper 状态,然后执行旧版 SSE,则您需要支付 一次性 惩罚才能过渡到保留的非 INIT 上限状态,但此后您无需支付任何罚款。

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

总体上惩罚较少,但对您的情况至关重要,其中一个是自循环:执行旧 SSE 的惩罚(图 11-2 中的 Penalty A)指令dirty upper 状态让你保持在那个状态。这就是发生在你身上的事情——任何 AVX 指令都会让你进入脏上层状态,这会减慢所有进一步的 SSE 执行。

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

Skylake 微架构实现了不同的状态机 比前几代管理 YMM 状态转换相关的 混合 SSE 和 AVX 指令。它不再保存整个 在“已修改”中执行 SSE 指令时的上 YMM 状态 和未保存”状态,但保存单个寄存器的高位。 因此,混合 SSE 和 AVX 指令将受到惩罚 与目标的部分寄存器相关性相关联 正在使用的寄存器和高位上的附加混合操作 目标寄存器。

因此,惩罚显然是相当大的——它必须始终混合高位以保留它们,并且它还使显然独立的指令变得依赖,因为对隐藏的高位存在依赖。例如xorpd xmm0, xmm0 不再打破对xmm0 先前值的依赖,因为结果实际上取决于ymm0 中隐藏的高位,xorpd 不会清除这些高位。后一种影响可能会影响您的表现,因为您现在将拥有非常长的依赖链,这在通常的分析中是无法预料的。

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

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

更新:根据下面 OP 的 comment,运行时链接器 ld 插入了违规 (AVX) 代码,并且 bug 已经存在。


1来自英特尔的optimization manual。

【讨论】:

太棒了!我首先阅读了没有 Skylake cmets 的旧版手册,然后阅读了新版本还不够远,这让我感到困惑。新版本的页面比旧版本少,这无济于事。我一定会追查有问题的库。 违规代码在 _dl_runtime_resolve_avx(), /lib64/ld-linux-x86-64.so.2 中。似乎这应该在 glibc 的下一个版本中自行解决:sourceware.org/bugzilla/show_bug.cgi?id=20495 不建议在 KNL 上使用足够有趣的 VZEROUPPER,但情况正在讨论中software.intel.com/en-us/forums/intel-isa-extensions/topic/… 为什么 OP 会在 main.cpp 而不是在 slow_function.cpp 中获得 avx 指令,除非他使用 AVX 编译 main.cpp 而没有使用 AVX 编译 slow_function.cpp?除非被告知,否则 GCC 不应插入 AVX 指令,因为它会在没有 AVX 的系统上生成 SIGILL @Zboson - 我没有看到 OP 正在用不同的 AVX 标志编译两个文件?他说,如果他启用 AVX 编译,他就不会遇到问题,这是有道理的,因为对 Skylake 的唯一惩罚是对传统 SSE 执行(惩罚 A)。此外,编译器不会插入指令(您不会通过检查二进制文件找到它们),而是由于运行时链接器内部调用的某些方法而在运行时发生,正如 Olivier 上面提到的(我添加了链接也到我回答的结尾)。【参考方案2】:

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

【讨论】:

您是本网站 [x86] 标签的英雄之一。该标签的***追随者在这里广泛引用您,因为您是 x86 处理器微架构细节的稀有来源之一。继续努力! @BeeOnRope,OP 说他在 Sandy Bridge 和 Ivy Bridge 上没有问题,只有在 Skylake 上。 OP 没有测试 Haswell。但阿格纳看到了哈斯韦尔的问题。所以我有点困惑,因为我希望 Haswell 在这种情况下表现得像 Sandy Bridge 和 Ivy Bridge。 有没有可能 Haswell 实际上表现得像 Skylake,但在 SKL 出来之前没有人描述这种行为?或者它有时会这样?有没有可能只是在 256b 执行单元的上半部分通电之前的预热期间的一个因素?也许在 AVX-256 指令缓慢的期间状态转换行为不同?我刚得到一个 SKL 台式机,并且可以使用 Haswell 笔记本电脑,所以我可能会抽出时间来测试一下。不幸的是,我无法与 IvB 或 SnB 进行比较,我认为它们的工作方式与您和英特尔所描述的一样。 Peter,当 VEX 和非 VEX 代码混合时,Haswell 的每次状态转换成本为 70 个时钟周期,就像 Sandy 和 Ivy Bridge 一样。 Skylake 对状态转换没有任何延迟,但我认为它具有与我为 Haswell 描述的相同的错误依赖性。 就像一个有趣的事实(现在上床睡觉,只是在挖掘,如果有人在乎,请联系我) - 似乎 Skylake 有/没有禁用循环流解码器的微码补丁会有所作为(SOMEHOW)也是 - 你不知道找出原因有多痛苦,但我现在可以可靠地得到结果,所以......就是这样。

以上是关于为啥在 Skylake 上没有 VZEROUPPER 时,这个 SSE 代码会慢 6 倍?的主要内容,如果未能解决你的问题,请参考以下文章

为啥我的 Intel Skylake / Kaby Lake CPU 在简单的哈希表实现中会出现神秘的 3 倍减速?

为什么Skylake比Broadwell-E在单线程内存吞吐量方面要好得多?

新 Skylake-X(Core i9、79xxX/XE)CPU 支持 AVX-512 扩展

全新的Skylake 英特尔I7-6700K深度玩评

How to install Windows 7 SP1 on Skylake

SkyLake平台安装ubuntu16.04.1(Alienware15r2)