J'ai été à essayer de comprendre un problème de performance dans une application, et enfin rétréci vers le bas à vraiment un problème bizarre. Le morceau de code suivant s'exécute 6 fois plus lent sur un Skylake CPU (i5-6500) si l' VZEROUPPER
instruction est commenté. J'ai testé Sandy Bridge et Ivy Bridge Cpu et les deux versions tournent à la même vitesse, avec ou sans VZEROUPPER
.
Maintenant, j'ai une assez bonne idée de ce qu' VZEROUPPER
, et je pense qu'il ne devrait pas d'importance à ce code lorsqu'il n'y a pas VEX instructions codées et pas des appels à n'importe quelle fonction qui pourrait les contenir. Le fait qu'il n'est pas sur d'autres AVX capable Processeurs apparaît à l'appui de cette. Ne sorte de tableau 11-2 dans la technologie Intel® 64 et IA-32 Optimisation des Architectures Manuel
Donc ce qui se passe?
La seule théorie que j'ai quitté c'est qu'il y a un bug dans le CPU et il est correctement déclenchement de la "sauver la moitié supérieure de l'AVX registres" procédure où il ne devrait pas. Ou quelque chose d'autre tout aussi étrange.
C'est 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;
}
et c'est 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;
}
La fonction compile vers le bas pour cette avec 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
Le code généré est différent avec gcc, mais il montre le même problème. Une ancienne version de intel compilateur génère encore une autre variation de la fonction qui montre le problème aussi, mais seulement si main.cpp
n'est pas compilé avec le compilateur intel comme il insère des appels d'initialiser certaines de ses propres bibliothèques qui probablement finir par faire de la VZEROUPPER
quelque part.
Et bien sûr, si le tout est construit avec AVX de soutien de sorte que la intrinsèques sont transformés en VEX instructions codées, il n'y a pas de problème non plus.
J'ai essayé de profilage avec le code perf
sur linux et la plupart du runtime généralement sur les terres 1-2 instructions, mais pas toujours les mêmes selon la version du code que j'ai le profil (gcc, clang, intel). Le raccourcissement de la fonction s'affiche pour faire la différence de performances progressivement disparaître, de sorte qu'il ressemble de plusieurs instructions sont à l'origine du problème.
EDIT: Voici un pur assemblage de version pour linux. Les commentaires ci-dessous.
.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
Ok, donc on soupçonne la présence, dans les commentaires, à l'aide de VEX instructions codées causes du ralentissement. À l'aide de VZEROUPPER
efface jusqu'à. Mais cela ne fonctionne toujours pas expliquer pourquoi.
Comme je le comprends, ne pas utiliser VZEROUPPER
est censé entraîner un coût de transition à l'ancien jeu d'instructions SSE, mais un ralentissement de leur. Surtout pas comme un grand. La prise de la boucle de la surcharge en compte, le ratio est d'au moins 10x, peut-être plus.
J'ai essayé de jouer avec l'ensemble un peu et float instructions sont tout aussi mauvais que les doubles. Je ne pouvais pas identifier le problème à une seule instruction soit.