我一直在尝试找出应用程序中的性能问题,最后将其范围缩小到一个非常奇怪的问题。如果将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编译器构建的,因为它会插入调用以初始化其自己的某些库,而这些库可能最终在某处执行VZEROUPPER
。当然,如果整个组件都具有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
好的,正如注释中所怀疑的那样,使用VEX编码指令会导致速度变慢。使用
VZEROUPPER
将其清除。但这仍然不能解释原因。据我了解,不使用
VZEROUPPER
可能会导致过渡到旧的SSE指令的成本增加,但不会永久降低它们的速度。特别是没有那么大的一个。考虑到循环开销,该比率至少为10倍,甚至可能更高。我试过弄乱了程序集,而float指令和double指令一样糟糕。我也无法将问题确定为一条指令。
最佳答案
即使您的整个可见应用程序显然不使用任何AVX指令,您也要因“混合”非VEX SSE和VEX编码的指令而受到惩罚!
在Skylake之前,从使用vex的代码切换到不使用vex的代码时,这种惩罚只是一次过渡惩罚,反之亦然。也就是说,除非您积极混合使用VEX和非VEX,否则您永远不会为过去发生的任何事情支付持续的罚款。但是,在Skylake中,存在一种状态,即即使不进行进一步混合,非VEX SSE指令也会付出很高的持续执行代价。
直接从马口开始,这是图11-1 1-旧的(Skylake之前的)过渡图:
如您所见,所有处罚(红色箭头)都将您带入新的状态,此时不再需要重复执行该操作。例如,如果您通过执行一些256位AVX进入肮脏的上层状态,然后又执行旧版SSE,则需要一次性支付罚款以过渡到保留的非INIT上层状态,但是您无需支付费用在那之后的任何处罚。
在Skylake中,每个都不同,如图11-2 所示:
总体而言,惩罚较少,但对于您的情况而言,关键是其中之一是自我循环:在肮脏的较高状态下执行旧版SSE的惩罚(在图11-2中为惩罚指令)使您始终处于这种状态。州。这就是您所发生的事情-任何AVX指令都会使您进入肮脏的上层状态,这会减慢所有SSE的执行速度。
这是英特尔关于新处罚的规定(第11.3节):
因此,代价显然是非常大的-它必须一直混合最高位以保留它们,而且由于明显地依赖于隐藏的高位,因此还使得显然独立的指令变得依赖。例如xorpd xmm0, xmm0
不再打破对xmm0
先前值的依赖,因为结果实际上取决于ymm0
不会清除的xorpd
中隐藏的高位。后者的影响很可能会损害您的性能,因为您现在将拥有很长的依赖链,而通常的分析无法预期。
这是最糟糕的性能陷阱之一:现有体系结构的行为/最佳实践与当前体系结构本质上相反。据推测,硬件架构师有充分的理由进行更改,但是确实在细微的性能问题列表中增加了另一个“陷阱”。
我将针对插入该AVX指令且未跟进VZEROUPPER
的编译器或运行时提出错误。
更新:对于以下OP的comment,运行时链接程序ld
插入了有问题的(AVX)代码,并且bug已经存在。
1来自Intel的optimization manual。