64 votes

Pourquoi ce code SSE est 6 fois plus lent sans VZEROUPPER sur Skylake?

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.

80voto

BeeOnRope Points 3617

Vous êtes victime d'une pénalité pour "mélange" non-VEX de l'ESS et de VEX codé instructions - même si votre application n'est bien évidemment pas utiliser toutes les instructions AVX!

Avant Skylake, ce type de sanction a été qu'une seule fois à la transition de pénalité, lors de la commutation de code utilisé vex de code qui n'a pas, ou vice-versa. C'est, vous n'avez jamais payé un cours de pénalité pour ce qui est arrivé dans le passé, à moins que vous ont été activement mélange de VEX et non-VEX. Dans Skylake, cependant, il est un état où les non-VEX instructions SSE élevé en cours d'exécution de peine, sans plus de mélange.

Directement de la bouche des chevaux, voici la Figure 11-1 1 - l'ancien (pré-Skylake) de la transition du diagramme:

Pre-Skylake Transition Penalties

Comme vous pouvez le voir, toutes les sanctions (flèches rouges), vous amener à un état nouveau, à quel point il n'est plus une pénalité pour la répétition de cette action. Par exemple, si vous arrivez à la sale supérieur de l'état, par l'exécution de certains 256 bits AVX, un ensuite exécuter l'héritage de l'ESS, vous payez une seule fois à peine à faire la transition à la préservé non-INIT supérieur de l'état, mais vous n'avez pas à payer de pénalités après.

Dans Skylake, tout est différent par la Figure 11-2:

Skylake Penalties

Il y a moins de sanctions dans l'ensemble, mais de manière critique pour votre cas, l'un d'eux est un auto-boucle: la pénalité pour l'exécution d'un héritage de l'ESS (Peine dans la Figure 11-2) instruction dans la sale supérieur de l'etat vous permet de rester en cet état. C'est ce qui se produit pour vous, toute instruction AVX vous met dans la sale supérieur de l'état, ce qui ralentit tous les autres de l'ESS exécution.

Voici ce que Intel dit (section 11.3) à propos de la nouvelle peine:

Les Skylake microarchitecture implémente un état différent de l'état de la machine que les générations précédentes pour gérer la YMM état de transition associées avec le mélange de l'ESS et des instructions AVX. Il n'enregistre plus la totalité de la supérieure YMM de l'état lors de l'exécution d'une instruction SSE en "Modifié et non enregistrées" de l'état, mais permet d'économiser les bits de poids de registre. En conséquence, le mélange de l'ESS et des instructions AVX ferez l'expérience d'une pénalité associé avec partielle registre de la dépendance de la destination les registres utilisés et de supplémentaires de fusion de l'opération sur les bits de poids de la destination des registres.

De sorte que la pénalité est apparemment assez grand, il mélange le haut de bits de tous les temps pour les préserver, et il fait aussi des instructions qui sont apparemment indépendamment de devenir dépendant, car il y a une dépendance sur le caché bits de poids. Par exemple xorpd xmm0, xmm0 ne casse plus la dépendance de la valeur précédente de xmm0, puisque le résultat est en fait dépendante de l'caché supérieure bits de ymm0 qui ne sont pas compensés par l' xorpd. Ce dernier effet est probablement ce qui tue votre performance puisque vous aurez maintenant très longtemps dépendance des chaînes qui ne m'attends pas à partir de l'analyse classique.

C'est parmi les pires performances écueil: d'où le comportement/les meilleures pratiques pour l'avant architecture est essentiellement à l'opposé de l'architecture actuelle. Sans doute que le matériel architectes avaient une bonne raison de faire le changement, mais il n'a tout simplement ajouter un autre "piège" de la liste de subtils problèmes de performances.

Je voudrais soumettre un rapport de bogue à l'encontre de la compilation ou de l'exécution qui a inséré cette AVX d'instruction et n'a pas suivi avec un VZEROUPPER.

Mise à jour: Par le po de commentaire ci-dessous, la récidive (AVX) code a été inséré par l'éditeur de lien dynamiques ld et un bug existe déjà.


1 à Partir d'Intel manuel d'optimisation.

38voto

A Fog Points 614

Je viens de faire quelques expériences (sur un Haswell). La transition entre des états propres et sales n'est pas coûteuse, mais l'état sale rend chaque opération vectorielle non-VEX dépendante de la valeur précédente du registre de destination. Dans votre cas, par exemple movapd %xmm1, %xmm5 aura une fausse dépendance sur ymm5 qui empêche l'exécution dans le désordre. Cela explique pourquoi vzeroupper est nécessaire après le code AVX.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X