我一直试图弄清楚应用程序中的性能问题,并最终将其缩小到一个非常奇怪的问题。如果注释掉VZEROUPPER
指令,则下面的代码在Skylake CPU(i5-6500)上运行速度慢6倍。我测试了Sandy Bridge和Ivy Bridge CPU,两个版本以相同的速度运行,有或没有VZEROUPPER
。
现在我对VZEROUPPER
的作用有了一个相当好的认识,我认为当没有VEX编码指令并且没有调用任何可能包含它们的函数时,对这段代码完全不重要。事实上它不支持其他支持AVX的CPU似乎支持这一点。 {11}在<{3}}
那是怎么回事?
我留下的唯一理论是,CPU中存在一个错误并且它错误地触发了&#34;保存AVX寄存器的上半部分&#34;它不应该的程序。或者别的什么就像奇怪一样。
这是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)。缩短功能似乎会使性能差异逐渐消失,因此看起来几条指令都会导致问题。
.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倍,可能更高。
我试过稍微搞乱程序集,浮动指令和双指令一样糟糕。我无法确定单个指令的问题。
答案 0 :(得分:33)
您正在遭受#34;混合&#34;非VEX SSE和VEX编码指令 - 即使您的整个可见应用程序显然不使用任何AVX指令!
在Skylake之前,当从使用vex的代码切换到没有代码的代码时,这种类型的惩罚只是一次转换惩罚,反之亦然。也就是说,除非您主动混合VEX和非VEX,否则您从未对过去发生的任何事情支付持续罚款。然而,在Skylake,有一种状态,非VEX SSE指令支付高额的持续执行惩罚,即使没有进一步混合。
直接离开马的嘴,这里的图11-1 1 - 旧的(前Skylake)过渡图:
正如你所看到的,所有的惩罚(红色箭头)都会带你进入一个新的状态,此时重复这个动作就不再受到惩罚了。例如,如果通过执行某些256位AVX进入 dirty upper 状态,然后执行旧版SSE,则需要支付一次性惩罚才能转换为保留非INIT上层状态,但在此之后你不会支付任何罚款。
在Skylake中,根据图11-2 :
,一切都不同整体惩罚较少,但对你的情况来说很严重,其中一个是自我循环:执行传统SSE(图11-2中的惩罚A )指令的惩罚脏上层状态使您保持该状态。这就是你发生的事情 - 任何AVX指令都会让你进入脏的高位状态,这会减慢所有进一步的SSE执行速度。
以下是英特尔所说的关于新惩罚的内容(第11.3节):
Skylake微体系结构实现了一种不同的状态机 比上一代管理YMM状态转换相关联 混合SSE和AVX指令。它不再拯救整个 在“已修改”中执行SSE指令时的上YMM状态 和未保存的“状态,但保存单个寄存器的高位。 因此,混合SSE和AVX指令将受到惩罚 与目的地的部分寄存器依赖关联 正在使用的寄存器和高位的附加混合操作 目的地登记册。
因此惩罚显然非常大 - 它必须始终将顶部位混合以保留它们,并且它还使得显然独立地成为依赖的指令,因为存在对隐藏的高位的依赖性。例如,xorpd xmm0, xmm0
不再打破对xmm0
之前值的依赖,因为结果实际上取决于ymm0
的隐藏高位,而xorpd
未被VZEROUPPER
清除{1}}。后一种效应可能会影响你的表现,因为你现在拥有很长的依赖链,而这种依赖链并不是通常的分析所期望的。
这是最糟糕的性能陷阱之一:先前架构的行为/最佳实践与当前架构基本相反。据推测,硬件架构师有充分的理由进行更改,但它只是添加了另一个&#34; gotcha&#34;到微妙的性能问题列表。
我会针对插入该AVX指令的编译器或运行时提交错误,并且没有跟进ld
。
更新:根据下面的OP comment,运行时链接器{{1}}和bug插入了违规(AVX)代码已经存在。
1 来自英特尔的optimization manual。
答案 1 :(得分:15)
我刚做了一些实验(在Haswell上)。干净状态和脏状态之间的转换并不昂贵,但是脏状态使得每个非VEX向量操作都依赖于目标寄存器的先前值。在您的情况下,例如movapd %xmm1, %xmm5
将对ymm5
具有错误依赖性,从而防止无序执行。这解释了为什么在AVX代码之后需要vzeroupper
。