我一直在尝试解决一个应用程序的性能问题,并最终将其缩小到了一个非常奇怪的问题。如果注释掉 VZEROUPPER
指令,下面的代码块在 Skylake CPU(i5-6500)上运行速度会慢 6 倍。我已经测试过 Sandy Bridge 和 Ivy Bridge CPU,两个版本都可以在相同的速度下运行,无论是否使用 VZEROUPPER
。
现在我对 VZEROUPPER
的作用有了相当好的了解,并且认为当没有 VEX 编码指令和任何可能包含这些指令的函数调用时,它对于这段代码来说根本不重要。事实上,其他 AVX 能力的 CPU 上也是如此。 Intel® 64 和 IA-32 架构优化参考手册 中的表 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不同,但显示了相同的问题。旧版本的英特尔编译器生成了另一种函数变体,也显示了该问题,但仅当 main.cpp
未使用英特尔编译器构建时,它插入调用以初始化其自己的某些库,这可能会在某个地方执行 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倍,也许更高。
我已经尝试过对汇编进行一些调整,浮点指令和双精度指令一样糟糕。我也无法将问题定位到单个指令。
/lib64/ld-linux-x86-64.so.2
。 - Z bosonmain()
中有一个printf()
,而没有则很快。 我用stepi在gdb中跟踪,并快速进入那个充满avx代码而没有vzeroupper的函数。 几次搜索后,我找到了glibc问题,它明确表示存在问题。 我后来发现memset()
同样存在问题,但不知道原因(代码看起来没问题)。 - Olivier