98 votes

Amélioration de REP MOVSB pour memcpy

Je voudrais utiliser l'ERMSB (Enhanced REP MOVSB) pour obtenir une bande passante élevée pour un memcpy personnalisé.

L'ERMSB a été introduit avec la microarchitecture Ivy Bridge. Consultez la section "Opération Enhanced REP MOVSB et STOSB (ERMSB)" dans le manuel d'optimisation d'Intel si vous ne savez pas ce qu'est l'ERMSB.

La seule manière que je connaisse de le faire directement est avec l'assemblage inline. J'ai obtenu la fonction suivante de https://groups.google.com/forum/#!topic/gnu.gcc.help/-Bmlm_EG_fE

static inline void *__movsb(void *d, const void *s, size_t n) {
  asm volatile ("rep movsb"
                : "=D" (d),
                  "=S" (s),
                  "=c" (n)
                : "0" (d),
                  "1" (s),
                  "2" (n)
                : "memory");
  return d;
}

Cependant, lorsque j'utilise cela, la bande passante est bien inférieure à celle de memcpy. __movsb atteint 15 Go/s et memcpy atteint 26 Go/s avec mon système i7-6700HQ (Skylake), Ubuntu 16.10, DDR4@2400 MHz dual channel 32 Go, GCC 6.2.

Pourquoi la bande passante est-elle beaucoup plus basse avec REP MOVSB? Que puis-je faire pour l'améliorer?

Voici le code que j'ai utilisé pour tester cela.

//gcc -O3 -march=native -fopenmp foo.c
#include 
#include 
#include 
#include 
#include 
#include 

static inline void *__movsb(void *d, const void *s, size_t n) {
  asm volatile ("rep movsb"
                : "=D" (d),
                  "=S" (s),
                  "=c" (n)
                : "0" (d),
                  "1" (s),
                  "2" (n)
                : "memory");
  return d;
}

int main(void) {
  int n = 1<<30;

  //char *a = malloc(n), *b = malloc(n);

  char *a = _mm_malloc(n,4096), *b = _mm_malloc(n,4096);
  memset(a,2,n), memset(b,1,n);

  __movsb(b,a,n);
  printf("%d\n", memcmp(b,a,n));

  double dtime;

  dtime = -omp_get_wtime();
  for(int i=0; i<10; i++) __movsb(b,a,n);
  dtime += omp_get_wtime();
  printf("dtime %f, %.2f Go/s\n", dtime, 2.0*10*1E-9*n/dtime);

  dtime = -omp_get_wtime();
  for(int i=0; i<10; i++) memcpy(b,a,n);
  dtime += omp_get_wtime();
  printf("dtime %f, %.2f Go/s\n", dtime, 2.0*10*1E-9*n/dtime);  
}

La raison pour laquelle je m'intéresse à rep movsb est basée sur ces commentaires

Remarquez que sur Ivybridge et Haswell, avec des tampons trop grands pour tenir dans le LLC, vous pouvez battre movntdqa en utilisant rep movsb; movntdqa entraîne un RFO dans le LLC, tandis que rep movsb ne le fait pas... rep movsb est significativement plus rapide que movntdqa lors de la diffusion en mémoire sur Ivybridge et Haswell (mais sachez que pré-Ivybridge il est lent!)

Qu'est-ce qui manque/sub-optimal dans cette implémentation de memcpy?


Voici mes résultats sur le même système à partir de tinymembnech.

 C copie en arrière                                     :   7910.6 Mo/s (1.4%)
 C copie en arrière (blocs de 32 octets)                    :   7696.6 Mo/s (0.9%)
 C copie en arrière (blocs de 64 octets)                    :   7679.5 Mo/s (0.7%)
 C copie                                               :   8811.0 Mo/s (1.2%)
 C copie prefetchée (pas de 32 octets)                    :   9328.4 Mo/s (0.5%)
 C copie prefetchée (pas de 64 octets)                    :   9355.1 Mo/s (0.6%)
 C copie en 2 passes                                        :   6474.3 Mo/s (1.3%)
 C copie en 2 passes prefetchée (pas de 32 octets)             :   7072.9 Mo/s (1.2%)
 C copie en 2 passes prefetchée (pas de 64 octets)             :   7065.2 Mo/s (0.8%)
 C remplissage                                               :  14426.0 Mo/s (1.5%)
 C remplissage (mélange dans des blocs de 16 octets)               :  14198.0 Mo/s (1.1%)
 C remplissage (mélange dans des blocs de 32 octets)               :  14422.0 Mo/s (1.7%)
 C remplissage (mélange dans des blocs de 64 octets)               :  14178.3 Mo/s (1.0%)
 ---
 memcpy standard                                      :  12784.4 Mo/s (1.9%)
 memset standard                                      :  30630.3 Mo/s (1.1%)
 ---
 copie MOVSB                                           :   8712.0 Mo/s (2.0%)
 copie MOVSD                                           :   8712.7 Mo/s (1.9%)
 copie SSE2                                            :   8952.2 Mo/s (0.7%)
 copie SSE2 sans temporalité                                :  12538.2 Mo/s (0.8%)
 copie SSE2 prefetchée (pas de 32 octets)                 :   9553.6 Mo/s (0.8%)
 copie SSE2 prefetchée (pas de 64 octets)                 :   9458.5 Mo/s (0.5%)
 copie SSE2 sans temporalité prefetchée (pas de 32 octets)     :  13103.2 Mo/s (0.7%)
 copie SSE2 sans temporalité prefetchée (pas de 64 octets)     :  13179.1 Mo/s (0.9%)
 copie SSE2 en 2 passes                                     :   7250.6 Mo/s (0.7%)
 copie SSE2 en 2 passes prefetchée (pas de 32 octets)          :   7437.8 Mo/s (0.6%)
 copie SSE2 en 2 passes prefetchée (pas de 64 octets)          :   7498.2 Mo/s (0.9%)
 copie SSE2 en 2 passes sans temporalité                         :   3776.6 Mo/s (1.4%)
 remplissage SSE2                                            :  14701.3 Mo/s (1.6%)
 remplissage SSE2 sans temporalité                                :  34188.3 Mo/s (0.8%)

Sur mon système, notez que copie SSE2 prefetchée est aussi plus rapide que copie MOVSB.


Dans mes tests initiaux, je n'ai pas désactivé le turbo. J'ai désactivé le turbo et testé à nouveau, mais cela ne semble pas faire beaucoup de différence. Cependant, changer la gestion de l'alimentation fait une grande différence.

Quand je fais

sudo cpufreq-set -r -g performance

Je vois parfois plus de 20 Go/s avec rep movsb.

avec

sudo cpufreq-set -r -g powersave

le meilleur que je vois est d'environ 17 Go/s. Mais memcpy ne semble pas être sensible à la gestion de l'alimentation.


J'ai vérifié la fréquence (en utilisant turbostat) avec et sans SpeedStep activé, avec performance et avec powersave pour l'arrêt, une charge sur un core et une charge sur 4 cores. J'ai exécuté la multiplication de matrices denses de l'Intel MKL pour créer une charge et j'ai défini le nombre de threads en utilisant OMP_SET_NUM_THREADS. Voici un tableau des résultats (nombres en GHz).

              SpeedStep     arrêt      1 core    4 cores
powersave     DÉSACTIVÉ           0.8       2.6       2.6
performance   DÉSACTIVÉ           2.6       2.6       2.6
powersave     ACTIVÉ            0.8       3.5       3.1
performance   ACTIVÉ            3.5       3.5       3.1

Cela montre qu'avec powersave même avec SpeedStep désactivé, le CPU se met toujours en fréquence de repos de 0.8 GHz. C'est seulement avec performance sans SpeedStep que le CPU tourne à une fréquence constante.

J'ai utilisé par exemple sudo cpufreq-set -r performance (car cpufreq-set donnait des résultats étranges) pour changer les paramètres d'alimentation. Cela réactive le turbo donc j'ai dû le désactiver ensuite.

0 votes

"Que puis-je faire pour l'améliorer?" ... fondamentalement rien. L'implémentation memcpy dans la version actuelle du compilateur est très probablement aussi proche de la solution optimale que vous pouvez obtenir avec une fonction générique. Si vous avez un cas spécial comme déplacer toujours exactement 15 octets/ect, alors peut-être qu'une solution asm personnalisée pourrait battre le compilateur gcc, mais si votre source C est assez explicite sur ce qui se passe (donnant de bons indices au compilateur sur l'alignement, la longueur, etc), le compilateur produira très probablement un code machine optimal même pour ces cas spécialisés. Vous pouvez essayer d'améliorer d'abord la sortie du compilateur.

7 votes

@KerrekSB Savez-vous ce qu'est le movsb rep amélioré ?

3 votes

@Ped7g, je n'attends pas que ce soit mieux que memcpy. Je m'attends à ce que ce soit aussi bon que memcpy. J'ai utilisé gdb pour parcourir memcpy et j'ai vu qu'il entre dans une boucle principale avec rep movsb. C'est donc apparemment ce que memcpy utilise de toute façon (dans certains cas).

2voto

Brendan Points 4614

En tant que guide général pour memcpy() :

a) Si les données copiées sont minuscules (moins de peut-être 20 octets) et ont une taille fixe, laissez-le compiler le faire. Raison : Le compilateur peut utiliser des instructions mov normales et éviter les frais généraux de démarrage.

b) Si les données copiées sont petites (moins d'environ 4 Ko) et sont garanties d'être alignées, utilisez rep movsb (si ERMSB est pris en charge) ou rep movsd (si ERMSB n'est pas pris en charge). Raison : Utiliser une alternative SSE ou AVX a énormément de "frais généraux de démarrage" avant de copier quoi que ce soit.

c) Si les données copiées sont petites (moins d'environ 4 Ko) et ne sont pas garanties d'être alignées, utilisez rep movsb. Raison : Utiliser SSE ou AVX, ou utiliser rep movsd pour la majeure partie et un peu de rep movsb au début ou à la fin, a trop de frais généraux.

d) Pour tous les autres cas, utilisez quelque chose comme ceci :

    mov edx,0
.again:
    pushad
.nextByte:
    pushad
    popad
    mov al,[esi]
    pushad
    popad
    mov [edi],al
    pushad
    popad
    inc esi
    pushad
    popad
    inc edi
    pushad
    popad
    loop .nextByte
    popad
    inc edx
    cmp edx,1000
    jb .again

Raison : Cela sera tellement lent que cela forcera les programmeurs à trouver une alternative qui n'implique pas de copier d'énormes quantités de données; et le logiciel résultant sera significativement plus rapide car la copie de grosses quantités de données a été évitée.

2 votes

Utiliser une alternative SSE ou AVX implique une "charge initiale" considérable avant de copier quoi que ce soit. De quoi s'agit-il exactement et pouvez-vous fournir plus de détails à ce sujet?

2 votes

@Zboson: Vérification si l'adresse de départ est correctement alignée ou non (pour la source et la destination), vérification si la taille est un multiple correct, vérification si rep movsb doit être utilisé de toute façon, etc (tout cela avec des prédictions de branches potentielles). Pour la plupart des processeurs, le SSE/AVX est désactivé pour économiser de l'énergie lorsque vous ne l'utilisez pas, donc vous pouvez être affecté par la "latence de démarrage du SSE/AVX". Ensuite, frais généraux d'appel de fonction (trop volumineux pour être en ligne), qui peuvent inclure la sauvegarde/la restauration de tout registre SSE/AVX utilisé par l'appelant. Enfin, si rien d'autre n'utilise le SSE/AVX, il y a une sauvegarde/restauration supplémentaire de l'état SSE/AVX lors des changements de tâche.

0 votes

@Zboson : Notez qu'il existe un "point de croisement" où l'amélioration des performances de copie des données l'emporte sur les frais généraux de tout ce désordre. Ce point de croisement varie (différents processeurs, etc.) mais c'est surtout toujours une question de "SSE / AVX ne vous aide que lorsque vous ne devriez pas copier autant de données en premier lieu".

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