为什么在 Skylake 上没有 VZEROUPPER 时此 SSE 代码会慢 6 倍?
Why is this SSE code 6 times slower without VZEROUPPER on Skylake?
我一直试图找出应用程序中的性能问题,并最终将其缩小为一个非常奇怪的问题。如果 VZEROUPPER
指令被注释掉,下面一段代码 运行s 在 Skylake CPU (i5-6500) 上慢 6 倍。我已经以相同的速度测试了 Sandy Bridge 和 Ivy Bridge CPUs 以及两个版本 运行,有或没有 VZEROUPPER
.
现在我对 VZEROUPPER
的作用有了一个很好的了解,并且我认为当没有 VEX 编码指令并且没有调用任何可能包含它们的函数时,这段代码应该无关紧要。它在其他支持 AVX 的 CPU 上没有的事实似乎支持这一点。 Intel® 64 and IA-32 Architectures Optimization Reference Manual
中的 table 11-2 也是如此
所以这是怎么回事?
我剩下的唯一理论是 CPU 中存在错误,它错误地触发了 "save the upper half of the AVX registers" 过程,而这是不应该的。或者其他同样奇怪的东西。
这是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 [=12=]x7,%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 0000000, %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 [=13=]x1, %eax
int [=13=]x80
好的,正如评论中所怀疑的那样,使用 VEX 编码指令会导致速度变慢。使用 VZEROUPPER
清除它。但这仍然不能解释为什么。
据我了解,不使用 VZEROUPPER
应该会涉及转换到旧 SSE 指令的成本,但不会永久降低它们的速度。特别是没有这么大的。考虑到循环开销,这个比率至少是 10 倍,也许更多。
我试过稍微弄乱程序集,浮点指令和双精度指令一样糟糕。我也无法将问题确定为单个指令。
您正在经历 "mixing" 非 VEX SSE 和 VEX 编码指令的惩罚 - 即使您的整个可见应用程序显然没有使用任何 AVX 指令!
在 Skylake 之前,当从使用 vex 的代码切换到不使用 vex 的代码时,这种类型的惩罚只是一次性 transition 惩罚,反之亦然.也就是说,除非你积极混合 VEX 和非 VEX,否则你永远不会为过去发生的任何事情支付持续的罚款。然而,在 Skylake 中,存在一种状态,即使没有进一步混合,非 VEX SSE 指令也会付出高昂的持续执行惩罚。
直截了当,这里是 图 11-1 1 - 旧的(Skylake 之前的)转换图:
如您所见,所有的惩罚(红色箭头)都会将您带到一个新的状态,此时重复该动作不再有惩罚。例如,如果您通过执行一些 256 位 AVX 进入 dirty upper 状态,然后执行旧版 SSE,您需要支付 一次性 转换到 preserved non-INIT upper 状态的惩罚,但在那之后你不需要支付任何惩罚。
在 Skylake 中,图 11-2 的一切都不一样:
总体上惩罚较少,但对于您的情况至关重要,其中之一是自循环:执行遗留 SSE 的惩罚(图 11 中的惩罚 A -2) dirty upper 状态中的指令使您处于该状态。这就是发生在你身上的事情——任何 AVX 指令都会让你处于脏上层状态,这会减慢所有进一步的 SSE 执行速度。
以下是英特尔关于新惩罚的说法(第 11.3 节):
The Skylake microarchitecture implements a different state machine
than prior generations to manage the YMM state transition associated
with mixing SSE and AVX instructions. It no longer saves the entire
upper YMM state when executing an SSE instruction when in “Modified
and Unsaved” state, but saves the upper bits of individual register.
As a result, mixing SSE and AVX instructions will experience a penalty
associated with partial register dependency of the destination
registers being used and additional blend operation on the upper bits
of the destination registers.
所以惩罚显然相当大 - 它必须始终混合最高位以保留它们,并且它还使显然独立的指令变得依赖,因为存在对隐藏的高位的依赖。例如 xorpd xmm0, xmm0
不再打破对 xmm0
先前值的依赖,因为结果实际上依赖于 ymm0
中隐藏的高位,这些位未被 [=13] 清除=].后一种影响可能会降低您的性能,因为您现在将拥有非常长的依赖链,这在通常的分析中是意料之外的。
这是最严重的性能陷阱之一:behavior/best 先前架构的实践与当前架构基本相反。据推测,硬件架构师有充分的理由进行更改,但它只是在微妙的性能问题列表中添加了另一个 "gotcha"。
我会针对插入该 AVX 指令但没有跟进 VZEROUPPER
的编译器或运行时提交错误。
更新: 根据 OP 的 below, the offending (AVX) code was inserted by the runtime linker ld
and a bug 已经存在。
1 来自英特尔的 optimization manual.
我刚刚做了一些实验(在 Haswell 上)。干净和脏状态之间的转换并不昂贵,但脏状态使每个非 VEX 向量操作都依赖于目标寄存器的先前值。在您的情况下,例如 movapd %xmm1, %xmm5
将对 ymm5
具有错误的依赖性,从而防止乱序执行。这解释了为什么在 AVX 代码之后需要 vzeroupper
。
我一直试图找出应用程序中的性能问题,并最终将其缩小为一个非常奇怪的问题。如果 VZEROUPPER
指令被注释掉,下面一段代码 运行s 在 Skylake CPU (i5-6500) 上慢 6 倍。我已经以相同的速度测试了 Sandy Bridge 和 Ivy Bridge CPUs 以及两个版本 运行,有或没有 VZEROUPPER
.
现在我对 VZEROUPPER
的作用有了一个很好的了解,并且我认为当没有 VEX 编码指令并且没有调用任何可能包含它们的函数时,这段代码应该无关紧要。它在其他支持 AVX 的 CPU 上没有的事实似乎支持这一点。 Intel® 64 and IA-32 Architectures Optimization Reference Manual
所以这是怎么回事?
我剩下的唯一理论是 CPU 中存在错误,它错误地触发了 "save the upper half of the AVX registers" 过程,而这是不应该的。或者其他同样奇怪的东西。
这是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 [=12=]x7,%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 0000000, %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 [=13=]x1, %eax
int [=13=]x80
好的,正如评论中所怀疑的那样,使用 VEX 编码指令会导致速度变慢。使用 VZEROUPPER
清除它。但这仍然不能解释为什么。
据我了解,不使用 VZEROUPPER
应该会涉及转换到旧 SSE 指令的成本,但不会永久降低它们的速度。特别是没有这么大的。考虑到循环开销,这个比率至少是 10 倍,也许更多。
我试过稍微弄乱程序集,浮点指令和双精度指令一样糟糕。我也无法将问题确定为单个指令。
您正在经历 "mixing" 非 VEX SSE 和 VEX 编码指令的惩罚 - 即使您的整个可见应用程序显然没有使用任何 AVX 指令!
在 Skylake 之前,当从使用 vex 的代码切换到不使用 vex 的代码时,这种类型的惩罚只是一次性 transition 惩罚,反之亦然.也就是说,除非你积极混合 VEX 和非 VEX,否则你永远不会为过去发生的任何事情支付持续的罚款。然而,在 Skylake 中,存在一种状态,即使没有进一步混合,非 VEX SSE 指令也会付出高昂的持续执行惩罚。
直截了当,这里是 图 11-1 1 - 旧的(Skylake 之前的)转换图:
如您所见,所有的惩罚(红色箭头)都会将您带到一个新的状态,此时重复该动作不再有惩罚。例如,如果您通过执行一些 256 位 AVX 进入 dirty upper 状态,然后执行旧版 SSE,您需要支付 一次性 转换到 preserved non-INIT upper 状态的惩罚,但在那之后你不需要支付任何惩罚。
在 Skylake 中,图 11-2 的一切都不一样:
总体上惩罚较少,但对于您的情况至关重要,其中之一是自循环:执行遗留 SSE 的惩罚(图 11 中的惩罚 A -2) dirty upper 状态中的指令使您处于该状态。这就是发生在你身上的事情——任何 AVX 指令都会让你处于脏上层状态,这会减慢所有进一步的 SSE 执行速度。
以下是英特尔关于新惩罚的说法(第 11.3 节):
The Skylake microarchitecture implements a different state machine than prior generations to manage the YMM state transition associated with mixing SSE and AVX instructions. It no longer saves the entire upper YMM state when executing an SSE instruction when in “Modified and Unsaved” state, but saves the upper bits of individual register. As a result, mixing SSE and AVX instructions will experience a penalty associated with partial register dependency of the destination registers being used and additional blend operation on the upper bits of the destination registers.
所以惩罚显然相当大 - 它必须始终混合最高位以保留它们,并且它还使显然独立的指令变得依赖,因为存在对隐藏的高位的依赖。例如 xorpd xmm0, xmm0
不再打破对 xmm0
先前值的依赖,因为结果实际上依赖于 ymm0
中隐藏的高位,这些位未被 [=13] 清除=].后一种影响可能会降低您的性能,因为您现在将拥有非常长的依赖链,这在通常的分析中是意料之外的。
这是最严重的性能陷阱之一:behavior/best 先前架构的实践与当前架构基本相反。据推测,硬件架构师有充分的理由进行更改,但它只是在微妙的性能问题列表中添加了另一个 "gotcha"。
我会针对插入该 AVX 指令但没有跟进 VZEROUPPER
的编译器或运行时提交错误。
更新: 根据 OP 的 ld
and a bug 已经存在。
1 来自英特尔的 optimization manual.
我刚刚做了一些实验(在 Haswell 上)。干净和脏状态之间的转换并不昂贵,但脏状态使每个非 VEX 向量操作都依赖于目标寄存器的先前值。在您的情况下,例如 movapd %xmm1, %xmm5
将对 ymm5
具有错误的依赖性,从而防止乱序执行。这解释了为什么在 AVX 代码之后需要 vzeroupper
。