Skylake上没有VZEROUPPER的SSE代码速度要慢6倍(2016)

2020-09-24 19:03:54

我一直在试图找出应用程序中的一个性能问题,最后将范围缩小到一个非常奇怪的问题。如果VZEROUPPER指令被注释掉,下面的代码在Skylake CPU(i5-6500)上的运行速度要慢6倍。我测试过Sandy Bridge和Ivy Bridge的CPU,无论有没有VZEROUPPER,这两个版本的运行速度都是一样的。

现在我对VZEROUPPER的作用有了相当好的了解,我认为当没有VEX编码的指令并且没有调用任何可能包含它们的函数时,这对这段代码应该没有任何影响。它在其他支持AVX的CPU上不支持这一点,这一事实似乎支持这一点。英特尔®64和IA-32架构优化参考手册中的表11-2也是如此。

我剩下的唯一理论是,CPU中有一个错误,它错误地触发了AVX寄存器过程的上半部分,将其保存在本应保存的位置或其他同样奇怪的地方。

#include<;immintrin.h>;int low_function(Double i_a,Double i_b,Double I_c);int main(){/*daz and ftz,在此不会更改任何内容。*/_mm_setcsr(_mm_getcsr()|0x8040);/*此指令修复性能。*/__asm_Volatile__(";vzeroupper";:);int r=0;for(unsign j=0;j<;100000000;++j){r|=slow_function(0.84445079384884236262,-6.1000481519580951328,5.0302160279288017364);}返回r;}。

#include<;immintrin.h>;int low_function(双i_a,双i_b,双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 v掩码;const__m128d零=_mm_set零_pd();__m128d q_abc=_mm_add_sd(_mm_add_sd(q_a,q_b),q_c);if(_mm_comigt_sd(q_c,零)&;&;_mm_comigt_sd(q_abc,零)){返回7;}__m128d disr=_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_disr=_mm_sqrt_sd(disr,disr);__m128d q=sqrt_disr;__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);vask=_mm_movemask_pd(_mm_and_pd(_mm_cmplt_pd(0,v),_mm_cmple_pd(v,_mm_set1_pd(1.0);返回vask+1;}。

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<;_Z13low_function ddd+0x25>;e:66 0f 28 E9 movvd%xmm1,%xmm512:F2 0f 58 E8 addsd%xmm0,%xmm516:F2 0f 58 EA addsd%xmm2,%xmm51a:66 2f EB comisd%xmm3,%x51e:B8 00 00 mov$0x7,%eax23:77 6ja<;_Zmm512:F2 0f 58 E8 addsd%xmm0,%xmm516:F2 0f 58 EA addsd%xmm2,%xmm51a:66 2f EB comisd%xmm3,%x51e:B8 00 00 mov$0x7,%eax23:77 6ja<;25:F2 0f 59 c9 mulsd%xmm1,%xmm129:66 0f 28 E8 movold%xmm0,%xmm52d:F2 0f 59 2d 00 00 mulsd 0x0(%rip),%xmm5#35<;_Z13low_function ddd+0x35>;34:00 35:F2 0f 59 EA mulsd%xmm2,%xmm539:F2 0f 58 E9 addsd%xmm1,%xmm53d:F3 0f 7e CD movq%xmm5,%xmm141:F2 0f 51 c9 sqrtsd%xmm1,%xmm145:F3 0f 7e c9 movq%xmm1,%xmm149:66 0f 14 c1 unpcklpd%xmm1,%xmm4d:66 0f 14 cunpclpd%xmm4,%xmm151:66 0f 5e c8 divpd%xmm0,%xmm151:66 0f c2 d9 01 cmpltpd%xmm1,%xmm149:66 0f 14 c1 unpcklpd%xmm1,%xmm151:66 0f 5e c8 divpd%xmm0,%xmm151:66 0f c2 d9 cmpltpd%xmm1,%xmm149:66 0f 14 c1 cunpd%xmm0(%),%xmm151:66 0f 5e c8 divpd%xmm0,%xmm151:66 0f c2 d9 cmpltpd%xmm1%xmm1#63<;_Z13low_function ddd+0x63>;61:00 02 63:66 0f 54 CB和pd%xmm3,%xmm167:66 0f 50 C1 movmskpd%xmm1,%eax6b:FF c0 inc%eax6d:C3 retq。

生成的代码与GCC不同,但显示出相同的问题。旧版本的英特尔编译器生成函数的另一个变体,这也显示了问题,但只有当main.cpp不是用英特尔编译器构建的时候,因为它插入调用以初始化它自己的一些库,这可能最终在某个地方执行VZEROUPPER。

当然,如果整个系统都支持AVX,那么内部函数就变成了VEX编码的指令,这也是没有问题的。

我尝试过在linux上使用perf分析代码,大多数运行时通常使用1-2条指令,但并不总是相同的指令,这取决于我分析的代码的版本(GCC、叮当、英特尔)。缩短函数似乎会使性能差异逐渐消失,因此似乎有几条指令导致了问题。

.text.p2ign 4,0x90.globl_start_start:#vmovaps%ymm0,%ymm1#这会使SSE代码爬网。#vzeroupper#这又让它变得更快了。Movl$100000000,%eBP.p2Align 4,0x90.LBB0_1:xorpd%xmm0,%xmm0 xorpd%xmm1,%xmm1 xorpd%xmm2,%xmm2 movq%xmm2,%xmm4 xorpd%xmm3,%xmm3 movvd%xmm1,%xmm5 addsd%xmm0,%xmm5 addsd%xmmm2,%xmm1 movd%mmx0,%xmm5 movq%xmm2,%xmm5 movq%xmm5,%xmm1 mmmovq%mmx1,%xmm1 mmmovq%,%xxmm1 mmpunpd%xx1,%xmm1 movq%mmx1,%xmm1 mmmovq%mmx1,%xmm1 mmmovq%,%xmm1 mmpuncpd%xmm2,%xmm1 movq%mmx2,%xmm1 movq%xmmx1,%xmm1 mmmovq%mmx1,%xmm1 mmmovq%,%xmm1 mmpunpd%xx1,%xmm1 movpd%xmm2,%xmm5 movq%xmm5,%xmm1 movpd%mmx1,%xmm1 mmmovq%mm1。%xmm1下降%ebp jne.LBB0_1移动$0x1,%eax int$0x80。

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

根据我的理解,不使用VZEROUPPER应该涉及到转换到旧的SSE指令的成本,但不会永久地减慢它们的速度。尤其是不是这么大的一个。考虑到循环开销,这个比率至少是10倍,也许更多。

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

您使用的是什么编译器标志?也许(隐藏的)进程初始化正在使用一些VEX指令,这会使您处于一种无法退出的混合状态。您可以尝试复制/粘贴程序集,并使用_start将其构建为纯汇编程序,这样您就可以避免任何编译器插入的初始化代码,并查看它是否出现相同的问题。 --BeeOnRope。

@BeeOnRope我使用-O3-ffast-ath,但即使使用-O0,效果仍然存在。我将尝试纯组装。正如我刚刚在Agner的博客上发现的那样,你可能发现了一些事情,即在处理VEX过渡的方式上有了一些重大的内部变化。需要对此进行调查。 -大卫·奥利维尔(Olivier)。

我终于起身去看医生了。处罚在英特尔的手册中讨论得相当清楚,虽然Skylake的情况有所不同,但这并不一定更好-而在你的情况下,情况要糟糕得多。我在答案里加了细节。 --BeeOnRope

@Zboson AVX指令在动态链接器中,但我也不知道他们为什么要把它放在那里。请看我对BeeOnRope';答案的评论。这是一个相当丑陋的问题。 -大卫·奥利维尔(Olivier)。

@Zboson我认为我的测试用例在某种程度上很慢,在测试循环之前,main()中的printf()很慢,而在测试循环之前,测试用例很快。我用步骤I在gdb中跟踪,很快就进入了那个函数,里面充满了AVX代码,没有vzeroupper。几次搜索后,我发现了glibc问题,它清楚地表明那里存在问题。后来我发现memset()同样有问题,但是我不知道为什么(代码看起来没问题)。 -大卫·奥利维尔(Olivier)。

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

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

直接从马的嘴里出来,这里是图11-11-旧的(前Skylake)转换图:

正如你所看到的,所有的惩罚(红色箭头)都会把你带到一个新的状态,在这一点上,重复那个动作就不再有惩罚了。例如,如果您通过执行某个256位AVX进入肮脏的上层状态,然后执行遗留SSE,那么您将支付一次性罚金来转换到保留的非INIT上层状态,但是在此之后您不需要支付任何罚金。(#**$$}{##**$$}。

总体来说惩罚较少,但对您的情况至关重要的是,其中之一是自循环:在肮脏的较高状态下执行遗留SSE(图11-2中的惩罚A)指令的惩罚使您处于该状态。这就是发生在你身上的事情-任何AVX指令都会把你置于肮脏的上层状态,这会进一步减慢所有SSE的执行速度。

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

因此,惩罚显然是相当大的-它必须一直混合最高位来保存它们,而且它还使显然独立的指令变得依赖,因为存在对隐藏的高位的依赖。例如,xorpd xmm0,xmm0不再打破对先前xmm0值的依赖,因为结果实际上依赖于来自ymm0的隐藏的高位,这些位没有被xorpd清除。后一种影响可能会扼杀您的性能,因为您现在将有非常长的依赖链,这在通常的分析中是意想不到的。

这是最糟糕的性能陷阱类型之一:以前体系结构的行为/最佳实践基本上与当前体系结构相反。大概硬件架构师有很好的理由进行更改,但它确实只是在细微的性能问题清单上又增加了一个问题。

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

更新:根据OP';下面的评论,有问题的(AVX)代码是由运行时链接器ld插入的,错误已经存在。

太棒了!我先是读了没有Skylake注释的旧版手册,然后又读了不够远的新版本,这让我感到困惑。新版本的页数比旧版本少也无济于事。我一定会追查到违规的自由党。 -大卫·奥利维尔(Olivier)。

有问题的代码在_dl_runtime_Resolve_avx(),/lib64/ld-linux-x86-64.so.2中。似乎下一版本的glibc应该会自动解决这个问题:Sourceeware.org/bugzilla/show_bug.cgi?id=20495 -大卫·奥利维尔(Olivier)。

够有趣的是,在Knl上不推荐使用VZEROUPPER,但这种情况正在辩论software.intel.com/en-us/forums/intel-isa-extensions/topic/…。-Z玻色子。

除非OP使用AVX编译main.cpp,而不使用slow_function.cpp编译main.cpp,否则为什么OP在main.cpp中得到AVX指令,而不在low_function.cpp中得到AVX指令呢?GCC不应该插入avx指令,除非被告知,因为它会在没有avx的系统上生成SIGILL。 -Z玻色子

@Zboson-我没有看到操作员正在用不同的AVX标志编译这两个文件的任何地方?他说,如果他启用AVX编译,他就不会得到这个问题,这是有道理的,因为对Skylake的唯一惩罚是对遗留的上交所执行(惩罚A)。此外,指令不是由编译器插入的(您不会通过检查二进制文件找到它们),而是由于在运行时链接器中调用的某些方法而在运行时发生,正如Olivier在上面提到的(我还在答案的末尾添加了链接)。 --BeeOnRope。

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

您是本网站[x86]标签上的英雄之一。这个标签的狂热追随者在这里广泛引用了您的话,因为您是关于x86处理器微体系结构细节的少有消息来源之一。继续做好你的工作! -我将不存在任何文本主义者。

@BeeOnRope,操作员说他在桑迪桥和常春藤桥上没有问题,只有在Skylake上没有问题。这次行动没有测试哈斯韦尔。但阿格纳在哈斯韦尔身上看到了一个问题。所以我有点困惑,因为我希望哈斯韦尔在这件事上表现得像桑迪·布里奇和常青藤·布里奇。 -Z玻色子。

有没有可能哈斯韦尔的行为真的像Skylake,但在SKL问世之前没有人描述过这种行为?或者它有时会这样做?有没有可能在256b执行单元的上半部分通电之前,这只是一个热身过程中的一个因素?也许在AVX-256指令较慢的期间,状态转换行为不同?我刚买了一台SKL台式机,而且我可以使用一台哈斯韦尔笔记本电脑,所以我可能会抽出时间来测试一下。不幸的是,我无法与IVB或SNB相提并论,我认为它们的工作方式与您和英特尔的描述不谋而合。 -彼得·科德斯(Peter Cordes)。

彼得,当VEX和非VEX代码混合时,哈斯韦尔的每个状态转换的成本是70个时钟周期,就像桑迪和常春藤之桥一样。Skylake在状态转换上没有任何延迟,但我认为它具有与我为Haswell描述的错误依赖相同的错误依赖。 --“一场雾”

就像一个有趣的事实(现在上床,只要挖掘,如果有人关心我的话)-似乎Skylake有/没有微码补丁来禁用循环流解码器也有不同(以某种方式)-你不知道找出原因有多痛苦,但我现在可以可靠地得到一个结果了。就是这样。 -亚历克·蒂尔(Alec Teal)。

点击“发布您的答案”,即表示您同意我们的服务条款、隐私政策和Cookie政策

不是你想要的答案吗?浏览标记的其他问题或提出您自己的问题。