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).

134voto

BeeOnRope Points 3617

C'est un sujet qui me tient à cœur et qui a fait l'objet de recherches récentes. Je vais donc l'aborder sous plusieurs angles : l'histoire, quelques notes techniques (surtout académiques), les résultats des tests effectués sur mon boîtier et, enfin, une tentative de réponse à votre question actuelle : quand et où ? rep movsb pourrait avoir du sens.

En partie, il s'agit d'une appel à partager les résultats - si vous pouvez exécuter Tinymembench et partagez les résultats ainsi que les détails de votre configuration CPU et RAM, ce serait formidable. Surtout si vous avez une configuration à 4 canaux, une boîte Ivy Bridge, une boîte de serveur, etc.

Historique et avis officiel

L'histoire des performances des instructions de copie rapide de chaînes de caractères a été un peu une affaire de marches d'escalier - c'est-à-dire des périodes de stagnation des performances alternant avec des mises à jour importantes qui les ont alignées ou même plus rapides que les approches concurrentes. Par exemple, les performances ont fait un bond dans Nehalem (en ciblant principalement les frais de démarrage) et à nouveau dans Ivy Bridge (en ciblant principalement le débit total pour les grandes copies). Vous pouvez trouver des informations vieilles de dix ans sur les difficultés de la mise en œuvre du rep movs instructions d'un ingénieur d'Intel dans ce fil .

Par exemple, dans les guides qui ont précédé l'introduction de l'Ivy Bridge, le modèle typique de l'Ivy Bridge était le suivant conseils est de les éviter ou de les utiliser très soigneusement 1 .

Le guide actuel (enfin, juin 2016) contient une série de conseils confus et quelque peu incohérents, tels que 2 :

La variante spécifique de l'implémentation est choisie au moment de l'exécution en fonction de la disposition des données, de l'alignement et de la valeur du compteur (ECX). Pour Par exemple, MOVSB/STOSB avec le préfixe REP doit être utilisé avec une valeur de inférieure ou égale à trois pour de meilleures performances.

Donc pour les copies de 3 octets ou moins ? Vous n'avez pas besoin d'un rep en premier lieu, puisqu'avec une latence de démarrage annoncée de ~9 cycles, vous êtes presque certainement mieux loti avec un simple DWORD ou QWORD. mov avec un peu de manipulation pour masquer les octets inutilisés (ou peut-être avec 2 octets explicites, mot mov si vous savez que la taille est exactement de trois).

Ils poursuivent :

Les instructions String MOVE/STORE ont plusieurs granularités de données. Pour déplacement efficace des données, il est préférable d'utiliser des granularités de données plus grandes. Cela signifie qu'une meilleure efficacité peut être obtenue en décomposant une instruction de type valeur de compteur arbitraire en un certain nombre de doubles mots et d'octets uniques avec une valeur de comptage inférieure ou égale à 3.

Cela semble certainement faux sur le matériel actuel avec ERMSB où rep movsb est au moins aussi rapide, voire plus rapide, que le programme movd o movq variantes pour les grandes copies.

D'une manière générale, cette section (3.7.5) du guide actuel contient un mélange de conseils raisonnables et fortement obsolètes. Ceci est commun à tous les manuels Intel, puisqu'ils sont mis à jour de manière incrémentale pour chaque architecture (et prétendent couvrir près de vingt ans d'architectures, même dans le manuel actuel), et les anciennes sections ne sont souvent pas mises à jour pour remplacer ou rendre conditionnels les conseils qui ne s'appliquent pas à l'architecture actuelle.

Ils couvrent ensuite explicitement l'ERMSB dans la section 3.7.6.

Je ne vais pas passer en revue les autres conseils de manière exhaustive, mais je vais résumer les bonnes parties dans le "pourquoi l'utiliser" ci-dessous.

D'autres affirmations importantes du guide sont celles concernant Haswell, rep movsb a été amélioré pour utiliser des opérations de 256 bits en interne.

Considérations techniques

Il ne s'agit là que d'un résumé rapide des avantages et des inconvénients sous-jacents que l'on peut trouver dans le secteur de la santé. rep instructions ont d'un plan de mise en oeuvre .

Avantages pour rep movs

  1. Lorsqu'un rep movs est émise, le CPU connaît qu'un bloc entier d'une taille connue doit être transféré. Cela peut l'aider à optimiser l'opération d'une manière qu'il ne peut pas faire avec des instructions discrètes, par exemple :

    • Éviter la demande RFO lorsqu'il sait que la ligne de cache entière sera écrasée.
    • Émettre des requêtes prefetch immédiatement et exactement. Le préfixe matériel fait un bon travail pour détecter memcpy mais il faut encore quelques lectures pour qu'elle se mette en marche et elle "sur-préfetch" de nombreuses lignes de cache au-delà de la fin de la région copiée. rep movsb connaît exactement la taille de la région et peut préfixer exactement.
  2. Apparemment, il n'y a pas de garantie de commande parmi les magasins de l'Union européenne. 3 un seul rep movs ce qui peut contribuer à simplifier le trafic de cohérence et simplement d'autres aspects du déplacement du bloc, par rapport à une simple mov qui doivent obéir à un ordonnancement assez strict de la mémoire. 4 .

  3. En principe, le rep movs pourrait profiter de diverses astuces architecturales qui ne sont pas exposées dans l'ISA. Par exemple, les architectures peuvent avoir des chemins de données internes plus larges que ceux que l'ISA expose. 5 y rep movs pourrait l'utiliser en interne.

Inconvénients

  1. rep movsb doit mettre en œuvre une sémantique spécifique qui peut être plus forte que l'exigence logicielle sous-jacente. En particulier, memcpy interdit les régions qui se chevauchent, et peut donc ignorer cette possibilité, mais rep movsb les autorise et doit produire le résultat escompté. Dans les implémentations actuelles, cela affecte surtout le surcoût de démarrage, mais probablement pas le débit des gros blocs. De même, rep movsb doit prendre en charge les copies granulaires d'octets, même si vous l'utilisez en réalité pour copier de grands blocs qui sont un multiple d'une grande puissance de 2.

  2. Le logiciel peut disposer d'informations sur l'alignement, la taille de la copie et le crénelage éventuel qui ne peuvent pas être communiquées au matériel si l'on utilise la fonction rep movsb . Les compilateurs peuvent souvent déterminer l'alignement des blocs de mémoire 6 et peut ainsi éviter une grande partie du travail de démarrage qui rep movs doit faire sur chaque l'invocation.

Résultats des tests

Voici les résultats de tests pour de nombreuses méthodes de copie différentes de tinymembench sur mon i7-6700HQ à 2,6 GHz (dommage que j'aie le même processeur et que nous n'ayons pas un nouveau point de données...) :

 C copy backwards                                     :   8284.8 MB/s (0.3%)
 C copy backwards (32 byte blocks)                    :   8273.9 MB/s (0.4%)
 C copy backwards (64 byte blocks)                    :   8321.9 MB/s (0.8%)
 C copy                                               :   8863.1 MB/s (0.3%)
 C copy prefetched (32 bytes step)                    :   8900.8 MB/s (0.3%)
 C copy prefetched (64 bytes step)                    :   8817.5 MB/s (0.5%)
 C 2-pass copy                                        :   6492.3 MB/s (0.3%)
 C 2-pass copy prefetched (32 bytes step)             :   6516.0 MB/s (2.4%)
 C 2-pass copy prefetched (64 bytes step)             :   6520.5 MB/s (1.2%)
 ---
 standard memcpy                                      :  12169.8 MB/s (3.4%)
 standard memset                                      :  23479.9 MB/s (4.2%)
 ---
 MOVSB copy                                           :  10197.7 MB/s (1.6%)
 MOVSD copy                                           :  10177.6 MB/s (1.6%)
 SSE2 copy                                            :   8973.3 MB/s (2.5%)
 SSE2 nontemporal copy                                :  12924.0 MB/s (1.7%)
 SSE2 copy prefetched (32 bytes step)                 :   9014.2 MB/s (2.7%)
 SSE2 copy prefetched (64 bytes step)                 :   8964.5 MB/s (2.3%)
 SSE2 nontemporal copy prefetched (32 bytes step)     :  11777.2 MB/s (5.6%)
 SSE2 nontemporal copy prefetched (64 bytes step)     :  11826.8 MB/s (3.2%)
 SSE2 2-pass copy                                     :   7529.5 MB/s (1.8%)
 SSE2 2-pass copy prefetched (32 bytes step)          :   7122.5 MB/s (1.0%)
 SSE2 2-pass copy prefetched (64 bytes step)          :   7214.9 MB/s (1.4%)
 SSE2 2-pass nontemporal copy                         :   4987.0 MB/s

Quelques éléments clés à retenir :

  • El rep movs sont plus rapides que toutes les autres méthodes qui ne sont pas "non-temporelles". 7 et beaucoup plus rapide que les approches "C" qui copient 8 octets à la fois.
  • Les méthodes "non temporelles" sont plus rapides, jusqu'à environ 26 %, que les méthodes "temporelles". rep movs mais il s'agit d'un écart beaucoup plus faible que celui que vous avez indiqué (26 Go/s contre 15 Go/s = ~73 %).
  • Si vous n'utilisez pas de magasins non temporels, l'utilisation de copies de 8 octets à partir de C est à peu près aussi bonne que les chargements/stocks SSE de 128 bits. En effet, une bonne boucle de copie peut générer une pression mémoire suffisante pour saturer la bande passante (par exemple, 2,6 GHz * 1 magasin/cycle * 8 octets = 26 Go/s pour les magasins).
  • Il n'y a pas d'algorithmes 256 bits explicites dans tinymembench (à l'exception probablement de l'algorithme "standard"). memcpy ) mais cela n'a probablement pas d'importance en raison de la note ci-dessus.
  • L'augmentation du débit des approches de stockage non temporelles par rapport aux approches temporelles est d'environ 1,45x, ce qui est très proche du 1,5x auquel on pourrait s'attendre si NT élimine 1 transfert sur 3 (c'est-à-dire, 1 lecture, 1 écriture pour NT contre 2 lectures, 1 écriture). Le site rep movs les approches se situent au milieu.
  • La combinaison d'une latence mémoire assez faible et d'une modeste bande passante à deux canaux signifie que cette puce particulière est capable de saturer sa bande passante mémoire à partir d'un seul thread, ce qui change radicalement le comportement.
  • rep movsd semble utiliser la même magie que rep movsb sur cette puce. C'est intéressant car l'ERMSB ne cible explicitement que movsb et des tests antérieurs sur des arcs plus anciens avec ERMSB montrent movsb qui fonctionne beaucoup plus rapidement que movsd . Il s'agit là d'une question purement théorique puisque movsb est plus général que movsd de toute façon.

Haswell

En regardant le Résultats Haswell aimablement fourni par iwillnotexist dans les commentaires, nous constatons les mêmes tendances générales (résultats les plus pertinents extraits) :

 C copy                                               :   6777.8 MB/s (0.4%)
 standard memcpy                                      :  10487.3 MB/s (0.5%)
 MOVSB copy                                           :   9393.9 MB/s (0.2%)
 MOVSD copy                                           :   9155.0 MB/s (1.6%)
 SSE2 copy                                            :   6780.5 MB/s (0.4%)
 SSE2 nontemporal copy                                :  10688.2 MB/s (0.3%)

El rep movsb est toujours plus lente que l'approche non temporelle. memcpy mais seulement d'environ 14 % ici (contre ~26 % dans le test Skylake). L'avantage des techniques NT par rapport à leurs cousines temporelles est maintenant de ~57%, même un peu plus que l'avantage théorique de la réduction de la bande passante.

Quand devez-vous utiliser rep movs ?

Enfin, je réponds à votre question : quand ou pourquoi l'utiliser ? Il s'inspire de ce qui précède et introduit quelques nouvelles idées. Malheureusement, il n'y a pas de réponse simple : vous devrez arbitrer entre différents facteurs, dont certains que vous ne pouvez probablement même pas connaître exactement, comme les développements futurs.

Il convient de noter que l'alternative à rep movsb peut être la libc optimisée memcpy (y compris les copies inlined par le compilateur), ou bien il peut s'agir d'une version de memcpy version. Certains des avantages ci-dessous ne s'appliquent que par rapport à l'une ou l'autre de ces alternatives (par exemple, la "simplicité" est utile par rapport à une version roulée à la main, mais pas par rapport à une version intégrée). memcpy ), mais certaines s'appliquent aux deux.

Restrictions sur les instructions disponibles

Dans certains environnements, il existe une restriction sur certaines instructions ou sur l'utilisation de certains registres. Par exemple, dans le noyau Linux, l'utilisation des registres SSE/AVX ou FP est généralement interdite. Par conséquent, la plupart des programmes optimisés de memcpy ne peuvent pas être utilisées, car elles reposent sur les registres SSE ou AVX, et une simple version 64 bits de l'interface utilisateur. mov -La copie basée sur les données est utilisée sur x86. Pour ces plateformes, l'utilisation de rep movsb permet de bénéficier de la plupart des performances d'un memcpy sans briser la restriction sur le code SIMD.

Un exemple plus général pourrait être le code qui doit cibler plusieurs générations de matériel, et qui n'utilise pas de répartition spécifique au matériel (par exemple, l'utilisation de cpuid ). Dans ce cas, vous pourriez être contraint d'utiliser uniquement des jeux d'instructions plus anciens, ce qui exclut tout AVX, etc. rep movsb pourrait être une bonne approche ici puisqu'elle permet un accès "caché" à des chargements et des stockages plus larges sans utiliser de nouvelles instructions. Si vous ciblez du matériel pré-ERMSB, vous devrez voir si les instructions suivantes sont possibles rep movsb les performances y sont acceptables, cependant...

Préparer l'avenir

Un aspect intéressant de rep movsb c'est qu'elle le peut, en théorie profiter des améliorations architecturales sur les architectures futures, sans changement de source, ce que les déplacements explicites ne peuvent pas faire. Par exemple, lorsque les chemins de données de 256 bits ont été introduits, rep movsb a pu en tirer parti (comme le prétendait Intel) sans qu'il soit nécessaire de modifier le logiciel. Les logiciels utilisant des déplacements de 128 bits (ce qui était optimal avant Haswell) devaient être modifiés et recompilés.

Il s'agit donc à la fois d'un avantage pour la maintenance du logiciel (pas besoin de changer la source) et d'un avantage pour les binaires existants (pas besoin de déployer de nouveaux binaires pour profiter de l'amélioration).

L'importance de cet aspect dépend de votre modèle de maintenance (par exemple, la fréquence de déploiement des nouveaux binaires dans la pratique) et d'un jugement très difficile à porter sur la rapidité probable de ces instructions à l'avenir. Au moins, Intel guide les utilisations dans cette direction, en s'engageant à au moins raisonnable performance à l'avenir ( 15.3.3.6 ):

REP MOVSB et REP STOSB continueront à être raisonnablement performants sur les processeurs futurs.

Chevauchement avec des travaux ultérieurs

Cet avantage n'apparaîtra pas dans un simple memcpy Il s'agit bien sûr d'un point de référence qui, par définition, n'a pas de travail ultérieur à superposer, de sorte que l'ampleur de l'avantage devrait être soigneusement mesurée dans un scénario réel. Pour tirer le maximum d'avantages, il faudrait peut-être réorganiser le code entourant la fonction memcpy .

Cet avantage est souligné par Intel dans son manuel d'optimisation (section 11.16.3.4) et dans ses mots :

Lorsque l'on sait que le comptage est d'au moins mille octets ou plus, l'utilisation de l'utilisation du REP MOVSB/STOSB amélioré peut fournir un autre avantage pour amortir le coût du code non-consommateur. L'heuristique peut être comprise en utilisant une valeur de Cnt = 4096 et memset() comme exemple :

- Une implémentation SIMD 256 bits de memset() devra émettre/exécuter retirer 128 instances d'une opération de stockage de 32 octets avec VMOVDQA, avant que que les séquences d'instructions non consommatrices puissent se rendre à la retraite.

- Une instance de REP STOSB amélioré avec ECX= 4096 est décodée en tant qu'un long flux de micro-opérations fourni par le matériel, mais se retire en tant qu'une seule instruction. Il y a beaucoup d'opérations store_data qui doivent se terminer avant que le résultat de memset() puisse être consommé. Parce que l'achèvement l'achèvement de l'opération de stockage des données est découplé de la retraite de l'ordre du programme. une partie substantielle du flux de code non-consommateur peut être traitée à travers l'émission/exécution et le retrait, essentiellement sans coût si la séquence de si la séquence non consommatrice n'entre pas en concurrence avec les ressources du tampon de stockage.

Donc Intel dit qu'après tous les quelques uops le code après rep movsb a été émis, mais alors que de nombreux magasins sont encore en vol et que la rep movsb dans son ensemble n'a pas encore été retiré, les uops des instructions suivantes peuvent progresser davantage à travers la machinerie hors-ordre que si ce code venait après une boucle de copie.

Les uops d'une boucle explicite de chargement et de stockage doivent tous être retirés séparément dans l'ordre du programme. Cela doit se produire pour faire de la place dans le ROB pour les uops suivants.

Il ne semble pas y avoir beaucoup d'informations détaillées sur la façon dont une instruction microcodée très longue comme rep movsb le travail, exactement. Nous ne savons pas exactement comment les branches de microcodes demandent un flux différent d'uops au séquenceur de microcodes, ou comment les uops se retirent. Si les uops individuels n'ont pas à se retirer séparément, peut-être que l'instruction entière ne prend qu'un seul slot dans le ROB ?

Lorsque le frontal qui alimente la machine OoO voit une rep movsb dans le cache des uop, il active le Microcode Sequencer ROM (MS-ROM) pour envoyer des uop de microcode dans la file d'attente qui alimente l'étape d'émission/renommage. Il n'est probablement pas possible pour d'autres uop de se mêler à cette file et d'émettre/exécuter. 8 tandis que rep movsb est toujours en cours d'émission, mais les instructions suivantes peuvent être récupérées/décodées et émises juste après la dernière instruction rep movsb uop fait, alors qu'une partie de la copie n'a pas encore été exécutée. Ceci n'est utile que si au moins une partie de votre code ultérieur ne dépend pas du résultat de la fonction memcpy (ce qui n'est pas inhabituel).

Or, la taille de cet avantage est limitée : au maximum, vous pouvez exécuter N instructions (uops en fait) au-delà de la lenteur de l'ordinateur. rep movsb à laquelle vous décrocherez, où N est l'instruction Taille du ROB . Avec des tailles de ROB actuelles de ~200 (192 sur Haswell, 224 sur Skylake), cela représente un avantage maximal de ~200 cycles de travail gratuit pour le code suivant avec un IPC de 1. En 200 cycles, vous pouvez copier environ 800 octets à 10 Go/s, donc pour des copies de cette taille, vous pouvez obtenir un travail gratuit proche du coût de la copie (rendant en quelque sorte la copie gratuite).

Cependant, à mesure que la taille des copies augmente, l'importance relative de ce facteur diminue rapidement (par exemple, si vous copiez plutôt 80 Ko, le travail gratuit ne représente que 1 % du coût de la copie). Cela reste néanmoins intéressant pour les copies de taille modeste.

Les boucles de copie ne bloquent pas non plus totalement l'exécution des instructions suivantes. Intel ne donne pas de détails sur la taille de l'avantage, ni sur le type de copies ou de code environnant qui en bénéficie le plus. (Destination ou source chaude ou froide, code après haute ILP ou basse ILP à latence élevée).

Code Taille

La taille du code exécuté (quelques octets) est microscopique par rapport à un code optimisé typique. memcpy routine. Si les performances sont limitées par les manques dans le i-cache (y compris le uop cache), la réduction de la taille du code peut être bénéfique.

Là encore, nous pouvons délimiter l'ampleur de cet avantage en fonction de la taille de la copie. Je ne le calculerai pas numériquement, mais l'intuition est que la réduction de la taille du code dynamique de B octets permet d'économiser au maximum C * B cache-miss, pour une certaine constante C. Chaque appelez à memcpy subit le coût (ou le bénéfice) de l'absence de cache une fois, mais l'avantage d'un débit plus élevé s'échelonne avec le nombre d'octets copiés. Ainsi, pour les transferts importants, le débit plus élevé dominera les effets du cache.

Encore une fois, ce n'est pas quelque chose qui apparaîtra dans un benchmark ordinaire, où la boucle entière tiendra sans doute dans le cache uop. Vous aurez besoin d'un test réel, in-place, pour évaluer cet effet.

Optimisation spécifique à l'architecture

Vous l'avez signalé sur votre matériel, rep movsb était considérablement plus lente que la plateforme memcpy . Toutefois, même dans ce cas, des rapports font état du résultat inverse sur du matériel antérieur (comme Ivy Bridge).

C'est tout à fait plausible, puisqu'il semble que les opérations de déplacement de la chaîne de caractères soient améliorées périodiquement - mais pas à chaque génération, de sorte qu'il est possible qu'il soit plus rapide ou au moins à égalité (auquel cas il peut gagner sur la base d'autres avantages) sur les architectures où il a été mis à jour, pour ensuite prendre du retard sur le matériel suivant.

Citation : Andy Glew, qui devrait savoir une chose ou deux à ce sujet après avoir mis en œuvre ceux-ci sur le P6 :

la grande faiblesse de faire des chaînes rapides en microcode était [...] le microcode se déréglait avec chaque génération, devenant de plus en plus lent jusqu'à ce que quelqu'un se décide à le réparer. Tout comme une bibliothèque que les hommes d'une bibliothèque se désaccordait. Je suppose qu'il est possible que l'une des opportunités manquées a été d'utiliser des chargements et des stockages de 128 bits quand ils quand ils sont devenus disponibles, et ainsi de suite.

Dans ce cas, on peut considérer qu'il s'agit d'une optimisation "spécifique à la plate-forme" de plus à appliquer dans le cadre de l'approche typique du "tout-trick-dans-le-livre". memcpy que l'on trouve dans les bibliothèques standard et les compilateurs JIT : mais seulement pour une utilisation sur les architectures où c'est mieux. Pour les éléments compilés en JIT ou en AOT, c'est facile, mais pour les binaires compilés de manière statique, cela nécessite une distribution spécifique à la plate-forme, mais qui existe souvent déjà (parfois implémentée au moment de l'édition du lien), ou l'option de gestion de l'architecture. mtune peut être utilisé pour prendre une décision statique.

Simplicité

Même sur Skylake, où il semble qu'il ait pris du retard par rapport aux techniques non temporelles les plus rapides, il reste plus rapide que la plupart des approches et est très simple . Cela signifie moins de temps de validation, moins de bugs mystérieux, moins de temps de réglage et de mise à jour d'un monstre. memcpy (ou, à l'inverse, moins de dépendance vis-à-vis des caprices des implémenteurs de la bibliothèque standard si vous comptez sur celle-ci).

Plateformes à latence limitée

Algorithmes de limitation du débit mémoire 9 peut en fait fonctionner dans deux régimes généraux principaux : la bande passante de la DRAM ou la concurrence/latence.

Le premier mode est celui que vous connaissez probablement : le sous-système DRAM a une certaine largeur de bande théorique que vous pouvez calculer assez facilement en fonction du nombre de canaux, du débit/largeur de données et de la fréquence. Par exemple, mon système DDR4-2133 avec 2 canaux a une bande passante maximale de 2.133 * 8 * 2 = 34.1 GB/s, soit la même chose que rapporté sur ARK .

Vous ne supporterez pas plus que ce taux à partir de la DRAM (et généralement un peu moins en raison de diverses inefficacités) ajouté à tous les cœurs sur le socket (c'est-à-dire qu'il s'agit d'une limite globale pour les systèmes à un seul socket).

L'autre limite est imposée par le nombre de demandes simultanées qu'un cœur peut effectivement adresser au sous-système de mémoire. Imaginez qu'un cœur ne puisse avoir qu'une seule requête en cours à la fois, pour une ligne de cache de 64 octets - lorsque la requête est terminée, vous pouvez en émettre une autre. Supposons également une latence mémoire très rapide de 50ns. Alors, malgré la large bande passante DRAM de 34,1 Go/s, vous n'obtiendriez en fait que 64 octets / 50 ns = 1,28 Go/s, soit moins de 4 % de la bande passante maximale.

En pratique, les cœurs peuvent émettre plus d'une requête à la fois, mais pas un nombre illimité. Il est généralement entendu qu'il n'y a que 10 tampons de remplissage de ligne par cœur entre L1 et le reste de la hiérarchie mémoire, et peut-être 16 ou plus tampons de remplissage entre L2 et DRAM. Le prefetching est en concurrence pour les mêmes ressources, mais il permet au moins de réduire la latence effective. Pour plus de détails, consultez l'un des articles suivants Dr. Bandwidth a écrit sur le sujet principalement sur les forums Intel.

Encore, le plus Les processeurs récents sont limités par este et non la bande passante de la RAM. En général, ils atteignent 12 à 20 Go/s par cœur, alors que la bande passante de la RAM peut atteindre 50+ Go/s (sur un système à 4 canaux). Seuls certains cœurs "clients" récents à 2 canaux, qui semblent avoir un meilleur uncore, peut-être plus de tampons de ligne, peuvent atteindre la limite de DRAM sur un seul cœur, et nos puces Skylake semblent en faire partie.

Bien sûr, il y a une raison pour laquelle Intel conçoit des systèmes avec une bande passante DRAM de 50 Go/s, alors qu'ils ne peuvent supporter qu'une bande passante < 20 Go/s par cœur en raison des limites de concurrence : la première limite s'applique à l'ensemble du socket et la seconde à chaque cœur. Ainsi, chaque cœur d'un système à 8 cœurs peut envoyer des requêtes d'une valeur de 20 Go/s, après quoi il sera à nouveau limité par la DRAM.

Pourquoi je continue à parler de ça ? Parce que le meilleur memcpy La mise en œuvre dépend souvent du régime dans lequel vous opérez. Une fois que vous êtes limité par la bande passante de la DRAM (comme nos puces le sont apparemment, mais la plupart ne le sont pas sur un seul cœur), l'utilisation d'écritures non temporelles devient très importante car elle permet d'éviter la lecture pour la propriété qui gaspille normalement 1/3 de votre bande passante. Vous le voyez exactement dans les résultats des tests ci-dessus : les implémentations de memcpy qui Ne le fais pas. utilisent les magasins NT perdent 1/3 de leur bande passante.

En revanche, si la concurrence est limitée, la situation s'égalise et parfois s'inverse. Vous avez de la bande passante DRAM à revendre, donc les magasins NT n'aident pas et ils peuvent même nuire puisqu'ils peuvent augmenter la latence puisque le temps de transfert pour le tampon de ligne peut être plus long qu'un scénario où la pré-extraction amène la ligne RFO dans LLC (ou même L2) et ensuite le magasin se termine dans LLC pour une latence effective plus faible. Enfin, servidor Les uncores ont tendance à avoir des magasins NT beaucoup plus lents que ceux des clients (et une bande passante élevée), ce qui accentue cet effet.

Ainsi, sur d'autres plates-formes, vous pourriez trouver que les magasins NT sont moins utiles (au moins lorsque vous vous souciez de la performance d'un seul thread) et peut-être que rep movsb gagne où (si elle obtient le meilleur des deux mondes).

Vraiment, ce dernier point est un appel pour la plupart des tests. Je sais que les magasins NT perdent leur avantage apparent pour les tests monofilaires sur la plupart des archs (y compris les archs serveurs actuels), mais je ne sais pas comment rep movsb sera relativement performant...

Références

Autres bonnes sources d'information non intégrées dans ce qui précède.

enquête comp.arch de rep movsb contre les alternatives. Beaucoup de bonnes notes sur la prédiction des branches, et une implémentation de l'approche que j'ai souvent suggérée pour les petits blocs : utiliser des premières et/ou dernières lectures/écritures qui se chevauchent plutôt que d'essayer d'écrire seulement le nombre exact d'octets requis (par exemple, implémenter toutes les copies de 9 à 16 octets comme deux copies de 8 octets qui pourraient se chevaucher jusqu'à 7 octets).


1 L'intention est probablement de la limiter aux cas où, par exemple, la taille du code est très importante.

2 Voir Section 3.7.5 : Préfixe REP et mouvement des données.

3 Il est important de noter que cela ne s'applique qu'aux différents magasins de l'instruction unique elle-même : une fois terminé, le bloc de magasins apparaît toujours ordonné par rapport aux magasins précédents et suivants. Ainsi, le code peut voir les stockages de l'instruction rep movs hors d'usage les uns par rapport aux autres mais pas en ce qui concerne les magasins antérieurs ou postérieurs (et c'est cette dernière garantie dont vous avez généralement besoin). Ce ne sera un problème que si vous utilisez la fin de la destination de la copie comme un indicateur de synchronisation, au lieu d'un magasin séparé.

4 Notez que les magasins discrets non temporels évitent également la plupart des exigences d'ordonnancement, bien qu'en pratique rep movs a encore plus de liberté car il existe encore quelques contraintes de commande sur les magasins WC/NT.

5 C'était courant à la fin de l'ère 32 bits, lorsque de nombreuses puces disposaient de chemins de données 64 bits (par exemple, pour prendre en charge les FPU qui supportaient le 64 bits). double type). Aujourd'hui, les puces "neutrées" telles que les marques Pentium ou Celeron ont AVX désactivé, mais vraisemblablement rep movs Le microcode peut toujours utiliser des chargements/stockages de 256b.

6 Par exemple, en raison des règles d'alignement du langage, des attributs ou opérateurs d'alignement, des règles d'aliasing ou d'autres informations déterminées au moment de la compilation. Dans le cas de l'alignement, même si l'alignement exact ne peut être déterminé, ils peuvent au moins être en mesure d'extraire les contrôles d'alignement des boucles ou d'éliminer les contrôles redondants.

7 Je pars du principe que "standard" memcpy choisit une approche non temporelle, ce qui est très probable pour cette taille de tampon.

8 Ce n'est pas forcément évident, car il se pourrait que le flux d'uop généré par la commande rep movsb monopolise simplement la répartition et alors cela ressemblerait beaucoup à l'explicite mov le cas. Il semble cependant que cela ne fonctionne pas de cette manière - les uops des instructions suivantes peuvent se mélanger avec les uops de l'instruction microcodée. rep movsb .

9 C'est-à-dire ceux qui peuvent émettre un grand nombre de requêtes mémoire indépendantes et donc saturer la bande passante DRAM-cœur disponible, dont memcpy serait un exemple (par opposition aux charges purement liées à la latence, comme la recherche de pointeurs).

1 votes

ROB == tampon de réorganisation? Vous voudrez peut-être définir cet acronyme dès la première utilisation, sauf si vous l'avez déjà fait en haut et que je l'ai manqué.

1 votes

Tinymembench benchmark sur mon Haswell i7-4700MQ avec DDR3 à 1600 MHz.

0 votes

@CodyGray - en effet, oui. J'ai retravaillé cette phrase et inclus un lien vers une définition de ROB.

20voto

Maxim Masiutin Points 1454

REP MOVSB amélioré (Ivy Bridge et versions ultérieures)

Introduction de la microarchitecture Ivy Bridge (processeurs commercialisés en 2012 et 2013) Amélioré REP MOVSB (ERMSB). Nous devons encore vérifier le bit correspondant. ERMS était destiné à nous permettre de copier rapidement la mémoire avec rep movsb .

Les versions les moins chères des processeurs ultérieurs - Celeron et Pentium Kaby Lake, sortis en 2017, ne disposent pas d'AVX qui aurait pu être utilisé pour la copie rapide de la mémoire, mais disposent tout de même du REP MOVSB amélioré. Et certaines des architectures mobiles et basse consommation d'Intel sorties en 2018 et plus, qui n'étaient pas basées sur SkyLake, copient environ deux fois plus d'octets par cycle CPU avec REP MOVSB que les générations précédentes de microarchitectures.

Avant la microarchitecture Ice Lake, la fonction Enhanced REP MOVSB (ERMSB) avec Fast Short REP MOV (FSRM) n'était plus rapide que la copie AVX ou la copie de registre à usage général que si la taille du bloc est d'au moins 256 octets. Pour les blocs inférieurs à 64 octets, elle était beaucoup plus lent, car il y a un démarrage interne élevé dans ERMSB - environ 35 cycles. La fonction FSRM prévoit que les blocs avant 128 octets soient également rapides.

Voir le manuel Intel sur l'optimisation, section 3.7.6 Opération REP MOVSB et STOSB améliorée (ERMSB) http://www.intel.com/content/dam/www/public/us/en/documents/manuals/64-ia-32-architectures-optimization-manual.pdf (s'applique aux transformateurs qui ne disposaient pas encore de FSRM) :

  • Le coût de démarrage est de 35 cycles ;
  • les adresses source et destination doivent être alignées sur une frontière de 16 octets ;
  • la région source ne doit pas chevaucher la région de destination ;
  • la longueur doit être un multiple de 64 pour obtenir de meilleures performances ;
  • la direction doit être vers l'avant (CLD).

Comme je l'ai dit précédemment, REP MOVSB (sur les processeurs avant FSRM) commence à surpasser les autres méthodes lorsque la longueur est d'au moins 256 octets, mais pour voir le net avantage sur la copie AVX, la longueur doit être supérieure à 2048 octets. Il convient également de noter que la simple utilisation d'AVX (registres de 256 bits) ou d'AVX-512 (registres de 512 bits) pour la copie de mémoire peut parfois avoir des conséquences désastreuses, comme des pénalités de transition AVX/SSE ou une fréquence de turbo réduite. Le REP MOVSB est donc un moyen plus sûr qu'AVX pour copier la mémoire.

Sur l'effet de l'alignement si la copie REP MOVSB vs. AVX, le manuel Intel donne les informations suivantes :

  • si le tampon source n'est pas aligné, l'impact sur la mise en œuvre d'ERMSB par rapport à AVX 128 bits est similaire ;
  • si le tampon de destination n'est pas aligné, l'effet sur l'implémentation d'ERMSB peut être une dégradation de 25%, alors que l'implémentation AVX 128 bits de la copie de mémoire peut se dégrader de seulement 5%, par rapport au scénario aligné sur 16 octets.

J'ai fait des tests sur Intel Core i5-6600, sous 64-bit, et j'ai comparé l'implémentation de REP MOVSB memcpy() avec une simple implémentation de MOV RAX, [SRC] ; MOV [DST], RAX. lorsque les données correspondent au cache L1 :

REP MOVSB copie de la mémoire

 - 1622400000 data blocks of  32 bytes took 17.9337 seconds to copy;  2760.8205 MB/s
 - 1622400000 data blocks of  64 bytes took 17.8364 seconds to copy;  5551.7463 MB/s
 - 811200000 data blocks of  128 bytes took 10.8098 seconds to copy;  9160.5659 MB/s
 - 405600000 data blocks of  256 bytes took  5.8616 seconds to copy; 16893.5527 MB/s
 - 202800000 data blocks of  512 bytes took  3.9315 seconds to copy; 25187.2976 MB/s
 - 101400000 data blocks of 1024 bytes took  2.1648 seconds to copy; 45743.4214 MB/s
 - 50700000 data blocks of  2048 bytes took  1.5301 seconds to copy; 64717.0642 MB/s
 - 25350000 data blocks of  4096 bytes took  1.3346 seconds to copy; 74198.4030 MB/s
 - 12675000 data blocks of  8192 bytes took  1.1069 seconds to copy; 89456.2119 MB/s
 - 6337500 data blocks of  16384 bytes took  1.1120 seconds to copy; 89053.2094 MB/s

MOV RAX... copie de la mémoire

 - 1622400000 data blocks of  32 bytes took  7.3536 seconds to copy;  6733.0256 MB/s
 - 1622400000 data blocks of  64 bytes took 10.7727 seconds to copy;  9192.1090 MB/s
 - 811200000 data blocks of  128 bytes took  8.9408 seconds to copy; 11075.4480 MB/s
 - 405600000 data blocks of  256 bytes took  8.4956 seconds to copy; 11655.8805 MB/s
 - 202800000 data blocks of  512 bytes took  9.1032 seconds to copy; 10877.8248 MB/s
 - 101400000 data blocks of 1024 bytes took  8.2539 seconds to copy; 11997.1185 MB/s
 - 50700000 data blocks of  2048 bytes took  7.7909 seconds to copy; 12710.1252 MB/s
 - 25350000 data blocks of  4096 bytes took  7.5992 seconds to copy; 13030.7062 MB/s
 - 12675000 data blocks of  8192 bytes took  7.4679 seconds to copy; 13259.9384 MB/s

Ainsi, même sur des blocs de 128 bits, REP MOVSB (sur les processeurs avant FSRM) est plus lent qu'une simple copie MOV RAX dans une boucle (non déroulée). L'implémentation ERMSB commence à être plus performante que la boucle MOV RAX seulement à partir de blocs de 256 octets.

MOV rapide à court terme (FSRM)

La microarchitecture Ice Lake lancée en septembre 2019 a introduit la fonction Fast Short REP MOV (FSRM). Cette fonctionnalité peut être testée par un bit CPUID. Il était prévu que les chaînes de 128 octets et moins soient également rapides, mais, en fait, les chaînes avant 64 octets sont toujours plus lentes avec rep movsb qu'avec, par exemple, une simple copie de registre 64 bits. De plus, FSRM n'est implémenté que sous 64 bits, pas sous 32 bits. Du moins sur mon processeur i7-1065G7, rep movsb n'est rapide que pour les petites chaînes de caractères en 64 bits, mais en 32 bits, les chaînes de caractères doivent faire au moins 4KB pour que l'option rep movsb pour commencer à surpasser les autres méthodes.

MOVS normal (non amélioré) du REP sur Nehalem (2009-2013)

Étonnamment, les architectures précédentes (Nehalem et plus tard, jusqu'à Ivy Bridge non compris), qui ne disposaient pas encore de REP MOVB amélioré, avaient une mise en œuvre REP MOVSD/MOVSQ relativement rapide (mais pas REP MOVSB/MOVSW) pour les gros blocs, mais pas assez gros pour dépasser le cache L1.

Le manuel d'optimisation d'Intel (2.5.6 REP String Enhancement) donne l'information suivante est liée à la microarchitecture Nehalem - processeurs Intel Core i5, i7 et Xeon sortis en 2009 et 2010, et les microarchitectures ultérieures, y compris Sandy Bridge fabriqués jusqu'en 2013.

REP MOVSB

La latence pour le MOVSB est de 9 cycles si ECX < 4. Sinon, le REP MOVSB avec ECX > 9 a un coût de démarrage de 50 cycles.

  • tiny string (ECX < 4) : la latence de REP MOVSB est de 9 cycles ;
  • petite chaîne (ECX est entre 4 et 9) : aucune information officielle dans le manuel Intel, probablement plus de 9 cycles mais moins de 50 cycles ;
  • chaîne longue (ECX > 9) : coût de démarrage à 50 cycles.

MOVSW/MOVSD/MOVSQ

Citation du manuel d'optimisation d'Intel (2.5.6 REP String Enhancement) :

  • Chaîne courte (ECX <= 12) : la latence du REP MOVSW/MOVSD/MOVSQ est d'environ 20 cycles.
  • Chaîne rapide (ECX >= 76 : excluant REP MOVSB) : l'implémentation du processeur fournit une optimisation matérielle en déplaçant le plus grand nombre possible de données sur 16 octets. La latence de la chaîne REP varie si l'un des transferts de données de 16 octets s'étend sur la limite de la ligne de cache :
  • \= Split-free : la latence consiste en un coût de démarrage d'environ 40 cycles, et chaque 64 octets de données ajoute 4 cycles.
  • \= Le cache se divise : la latence consiste en un coût de démarrage d'environ 35 cycles, et chaque 64 octets de données ajoute 6 cycles.
  • Longueurs de chaîne intermédiaires : la latence du REP MOVSW/MOVSD/MOVSQ a un coût de démarrage d'environ 15 cycles plus un cycle pour chaque itération du mouvement de données en mot/mot/q-mot.

Par conséquent, selon Intel, pour les très grands blocs de mémoire, REP MOVSW est aussi rapide que REP MOVSD/MOVSQ. Quoi qu'il en soit, mes tests ont montré que seul REP MOVSD/MOVSQ est rapide, tandis que REP MOVSW est même plus lent que REP MOVSB sur Nehalem et Westmere.

Selon les informations fournies par Intel dans le manuel, sur les microarchitectures Intel précédentes (avant 2008), les coûts de démarrage sont encore plus élevés.

Conclusion : si vous avez juste besoin de copier des données qui correspondent au cache L1, seulement 4 cycles pour copier 64 octets de données est excellent, et vous n'avez pas besoin d'utiliser les registres XMM !

REP MOVSD/MOVSQ est la solution universelle qui fonctionne parfaitement sur tous les processeurs Intel (pas d'ERMSB nécessaire) si les données correspondent au cache L1 #.

Voici les tests de REP MOVS* lorsque la source et la destination étaient dans le cache L1, de blocs suffisamment grands pour ne pas être sérieusement affectés par les coûts de démarrage, mais pas trop grands pour ne pas dépasser la taille du cache L1. Source : http://users.atw.hu/instlatx64/

Yonah (2006-2008)

    REP MOVSB 10.91 B/c
    REP MOVSW 10.85 B/c
    REP MOVSD 11.05 B/c

Nehalem (2009-2010)

    REP MOVSB 25.32 B/c
    REP MOVSW 19.72 B/c
    REP MOVSD 27.56 B/c
    REP MOVSQ 27.54 B/c

Westmere (2010-2011)

    REP MOVSB 21.14 B/c
    REP MOVSW 19.11 B/c
    REP MOVSD 24.27 B/c

Ivy Bridge (2012-2013) - avec Enhanced REP MOVSB (tous les processeurs suivants ont également Enhanced REP MOVSB)

    REP MOVSB 28.72 B/c
    REP MOVSW 19.40 B/c
    REP MOVSD 27.96 B/c
    REP MOVSQ 27.89 B/c

SkyLake (2015-2016)

    REP MOVSB 57.59 B/c
    REP MOVSW 58.20 B/c
    REP MOVSD 58.10 B/c
    REP MOVSQ 57.59 B/c

Kaby Lake (2016-2017)

    REP MOVSB 58.00 B/c
    REP MOVSW 57.69 B/c
    REP MOVSD 58.00 B/c
    REP MOVSQ 57.89 B/c

J'ai présenté les résultats des tests pour SkyLake et Kaby Lake juste à titre de confirmation - ces architectures ont les mêmes données de cycle par instruction.

Cannon Lake, mobile (mai 2018 - février 2020)

    REP MOVSB 107.44 B/c
    REP MOVSW 106.74 B/c
    REP MOVSD 107.08 B/c
    REP MOVSQ 107.08 B/c

Cascade lake, serveur (avril 2019)

    REP MOVSB 58.72 B/c
    REP MOVSW 58.51 B/c
    REP MOVSD 58.51 B/c
    REP MOVSQ 58.20 B/c

Comet Lake, ordinateur de bureau, station de travail, mobile (août 2019).

    REP MOVSB 58.72 B/c
    REP MOVSW 58.62 B/c
    REP MOVSD 58.72 B/c
    REP MOVSQ 58.72 B/c

Ice Lake, mobile (septembre 2019)

    REP MOVSB 102.40 B/c
    REP MOVSW 101.14 B/c
    REP MOVSD 101.14 B/c
    REP MOVSQ 101.14 B/c

Tremont, faible puissance (septembre, 2020)

    REP MOVSB 119.84 B/c
    REP MOVSW 121.78 B/c
    REP MOVSD 121.78 B/c
    REP MOVSQ 121.78 B/c

Tiger Lake, mobile (octobre 2020)

    REP MOVSB 93.27 B/c
    REP MOVSW 93.09 B/c
    REP MOVSD 93.09 B/c
    REP MOVSQ 93.09 B/c

Comme vous le voyez, l'implémentation de REP MOVS diffère significativement d'une microarchitecture à l'autre. Sur certains processeurs, comme Ivy Bridge - REP MOVSB est le plus rapide, bien que juste un peu plus rapide que REP MOVSD/MOVSQ, mais il ne fait aucun doute que sur tous les processeurs depuis Nehalem, REP MOVSD/MOVSQ fonctionne très bien - vous n'avez même pas besoin de "Enhanced REP MOVSB", puisque, sur Ivy Bridge (2013) avec Amélioré REP MOVSB REP MOVSD montre les mêmes données d'octet par horloge que sur Nehalem (2010) sans Amélioré REP MOVSB En fait, le REP MOVSB n'est devenu très rapide que depuis SkyLake (2015) - deux fois plus rapide que sur Ivy Bridge. Donc ce Amélioré REP MOVSB dans le CPUID peut prêter à confusion - il indique seulement que le bit REP MOVSB en soi est acceptable, mais pas qu'une REP MOVS* est plus rapide.

L'implémentation ERMSB la plus déroutante se trouve sur la microarchitecture Ivy Bridge. Oui, sur les très anciens processeurs, avant ERMSB, les REP MOVS* pour les grands blocs utilisaient une fonctionnalité du protocole de cache qui n'est pas disponible pour le code normal (no-RFO). Mais ce protocole n'est plus utilisé sur les Ivy Bridge qui ont ERMSB. Selon Commentaires d'Andy Glew sur une réponse à "pourquoi les memcpy/memset compliqués sont-ils supérieurs ?" tirée d'une réponse de Peter Cordes Une fonction du protocole de cache qui n'est pas disponible pour le code ordinaire était autrefois utilisée sur les anciens processeurs, mais plus sur Ivy Bridge. Et il y a une explication de la raison pour laquelle les coûts de démarrage sont si élevés pour REP MOVS* : "L'importante surcharge pour choisir et mettre en place la bonne méthode est principalement due à l'absence de prédiction de branchement du microcode". Il est également intéressant de noter que le Pentium Pro (P6) de 1996 a mis en œuvre REP MOVS* avec des chargements et des stockages de microcode 64 bits et un protocole de cache no-RFO - ils n'ont pas violé l'ordonnancement de la mémoire, contrairement à ERMSB dans Ivy Bridge.

En ce qui concerne rep movsb vs rep movsq sur certains processeurs avec ERMSB rep movsb est légèrement plus rapide (par exemple, Xeon E3-1246 v3), sur d'autres rep movsq est plus rapide (Skylake), et sur l'autre c'est la même vitesse (par exemple i7-1065G7). Cependant, j'opterais pour rep movsq plutôt que rep movsb de toute façon.

Veuillez également noter que cette réponse n'est pertinente que pour les cas où les données source et destination correspondent au cache L1. Selon les circonstances, les particularités de l'accès à la mémoire (cache, etc.) doivent être prises en considération. Veuillez également noter que les informations contenues dans cette réponse ne concernent que les processeurs Intel et non les processeurs d'autres fabricants comme AMD qui peuvent avoir des implémentations meilleures ou pires des instructions REP MOVS*.

Résultats du Tinymembench

Voici quelques-uns des résultats de tinymembench pour montrer la performance relative de la rep movsb y rep movsd .

Intel Xeon E5-1650V3

Microarchitecture Haswell, ERMS, AVX-2, sortie en septembre 2014 pour 583 $, fréquence de base 3,5 GHz, fréquence turbo maximale : 3,8 GHz (un cœur), cache L2 6 × 256 Ko, cache L3 15 Mo, supporte jusqu'à 4×DDR4-2133, 8 modules de 32768 Mo DDR4 ECC reg installés (256 Go de RAM au total).

 C copy backwards                                     :   7268.8 MB/s (1.5%)
 C copy backwards (32 byte blocks)                    :   7264.3 MB/s
 C copy backwards (64 byte blocks)                    :   7271.2 MB/s
 C copy                                               :   7147.2 MB/s
 C copy prefetched (32 bytes step)                    :   7044.6 MB/s
 C copy prefetched (64 bytes step)                    :   7032.5 MB/s
 C 2-pass copy                                        :   6055.3 MB/s
 C 2-pass copy prefetched (32 bytes step)             :   6350.6 MB/s
 C 2-pass copy prefetched (64 bytes step)             :   6336.4 MB/s
 C fill                                               :  11072.2 MB/s
 C fill (shuffle within 16 byte blocks)               :  11071.3 MB/s
 C fill (shuffle within 32 byte blocks)               :  11070.8 MB/s
 C fill (shuffle within 64 byte blocks)               :  11072.0 MB/s
 ---
 standard memcpy                                      :  11608.9 MB/s
 standard memset                                      :  15789.7 MB/s
 ---
 MOVSB copy                                           :   8123.9 MB/s
 MOVSD copy                                           :   8100.9 MB/s (0.3%)
 SSE2 copy                                            :   7213.2 MB/s
 SSE2 nontemporal copy                                :  11985.5 MB/s
 SSE2 copy prefetched (32 bytes step)                 :   7055.8 MB/s
 SSE2 copy prefetched (64 bytes step)                 :   7044.3 MB/s
 SSE2 nontemporal copy prefetched (32 bytes step)     :  11794.4 MB/s
 SSE2 nontemporal copy prefetched (64 bytes step)     :  11813.1 MB/s
 SSE2 2-pass copy                                     :   6394.3 MB/s
 SSE2 2-pass copy prefetched (32 bytes step)          :   6255.9 MB/s
 SSE2 2-pass copy prefetched (64 bytes step)          :   6234.0 MB/s
 SSE2 2-pass nontemporal copy                         :   4279.5 MB/s
 SSE2 fill                                            :  10745.0 MB/s
 SSE2 nontemporal fill                                :  22014.4 MB/s

Intel Xeon E3-1246 v3

Haswell, ERMS, AVX-2, 3.50GHz

 C copy backwards                                     :   6911.8 MB/s
 C copy backwards (32 byte blocks)                    :   6919.0 MB/s
 C copy backwards (64 byte blocks)                    :   6924.6 MB/s
 C copy                                               :   6934.3 MB/s (0.2%)
 C copy prefetched (32 bytes step)                    :   6860.1 MB/s
 C copy prefetched (64 bytes step)                    :   6875.6 MB/s (0.1%)
 C 2-pass copy                                        :   6471.2 MB/s
 C 2-pass copy prefetched (32 bytes step)             :   6710.3 MB/s
 C 2-pass copy prefetched (64 bytes step)             :   6745.5 MB/s (0.3%)
 C fill                                               :  10812.1 MB/s (0.2%)
 C fill (shuffle within 16 byte blocks)               :  10807.7 MB/s
 C fill (shuffle within 32 byte blocks)               :  10806.6 MB/s
 C fill (shuffle within 64 byte blocks)               :  10809.7 MB/s
 ---
 standard memcpy                                      :  10922.0 MB/s
 standard memset                                      :  28935.1 MB/s
 ---
 MOVSB copy                                           :   9656.7 MB/s
 MOVSD copy                                           :   9430.1 MB/s
 SSE2 copy                                            :   6939.1 MB/s
 SSE2 nontemporal copy                                :  10820.6 MB/s
 SSE2 copy prefetched (32 bytes step)                 :   6857.4 MB/s
 SSE2 copy prefetched (64 bytes step)                 :   6854.9 MB/s
 SSE2 nontemporal copy prefetched (32 bytes step)     :  10774.2 MB/s
 SSE2 nontemporal copy prefetched (64 bytes step)     :  10782.1 MB/s
 SSE2 2-pass copy                                     :   6683.0 MB/s
 SSE2 2-pass copy prefetched (32 bytes step)          :   6687.6 MB/s
 SSE2 2-pass copy prefetched (64 bytes step)          :   6685.8 MB/s
 SSE2 2-pass nontemporal copy                         :   5234.9 MB/s
 SSE2 fill                                            :  10622.2 MB/s
 SSE2 nontemporal fill                                :  22515.2 MB/s (0.1%)

Intel Xeon Skylake-SP

Skylake, ERMS, AVX-512, 2.1 GHz

 MOVSB copy                                           :   4619.3 MB/s (0.6%)
 SSE2 fill                                            :   9774.4 MB/s (1.5%)
 SSE2 nontemporal fill                                :   6715.7 MB/s (1.1%)

Intel Xeon E3-1275V6

Kaby Lake, sorti en mars 2017 pour 339 dollars, fréquence de base 3,8 GHz, fréquence turbo maximale 4,2 GHz, cache L2 4 × 256 Ko, cache L3 8 Mo, 4 cœurs (8 threads), 4 modules de RAM de 16384 Mo DDR4 ECC installés, mais il ne peut utiliser que 2 canaux de mémoire.

 MOVSB copy                                           :  11720.8 MB/s
 SSE2 fill                                            :  15877.6 MB/s (2.7%)
 SSE2 nontemporal fill                                :  36407.1 MB/s

Intel i7-1065G7

Ice Lake, AVX-512, ERMS, FSRM, 1.37 GHz (fonctionne à la fréquence de base, mode turbo désactivé)

MOVSB copy                                           :   7322.7 MB/s
SSE2 fill                                            :   9681.7 MB/s
SSE2 nontemporal fill                                :  16426.2 MB/s

AMD EPYC 7401P

Commercialisé en juin 2017 au prix de 1075 $ US, basé sur la microarchitecture Zen gen.1, 24 cœurs (48 threads), fréquence de base : 2,0 GHz, turbo boost max : 3,0 GHz (quelques cœurs) ou 2,8 (tous les cœurs) ; cache : L1 - 64 KB inst. & 32 KB data par core, L2 - 512 KB par core, L3 - 64 MB, 8 MB par CCX, DDR4-2666 8 canaux, mais seulement 4 modules RAM de 32768 MB chacun de DDR4 ECC reg. installés.

 MOVSB copy                                           :   7718.0 MB/s
 SSE2 fill                                            :  11233.5 MB/s
 SSE2 nontemporal fill                                :  34893.3 MB/s

AMD Ryzen 7 1700X (4 modules de RAM installés)

 MOVSB copy                                           :   7444.7 MB/s
 SSE2 fill                                            :  11100.1 MB/s
 SSE2 nontemporal fill                                :  31019.8 MB/s

AMD Ryzen 7 Pro 1700X (2 modules de RAM installés)

 MOVSB copy                                           :   7251.6 MB/s
 SSE2 fill                                            :  10691.6 MB/s
 SSE2 nontemporal fill                                :  31014.7 MB/s

AMD Ryzen 7 Pro 1700X (4 modules de RAM installés)

 MOVSB copy                                           :   7429.1 MB/s
 SSE2 fill                                            :  10954.6 MB/s
 SSE2 nontemporal fill                                :  30957.5 MB/s

Conclusion

REP MOVSD/MOVSQ est la solution universelle qui fonctionne relativement bien sur tous les processeurs Intel pour les grands blocs de mémoire d'au moins 4KB (pas d'ERMSB nécessaire) si la destination est alignée d'au moins 64 octets. REP MOVSD/MOVSQ fonctionne encore mieux sur les processeurs plus récents, à partir de Skylake. Et, pour les microarchitectures Ice Lake ou plus récentes, il fonctionne parfaitement même pour les très petites chaînes d'au moins 64 octets.

2 votes

Données intéressantes de taille moyenne de tampon L1D. Ce n'est peut-être pas toute l'histoire, cependant. Certains des avantages de ERMSB (comme un plus faible ordre des magasins) ne se manifesteront que avec des tampons plus grands qui ne rentrent pas dans le cache. Même les chaînes rapides normales rep movs devraient utiliser un protocole sans RFO, même sur les CPU pré-ERMSB.

3 votes

Si je comprends bien, vous avez simplement extrait les chiffres L1D-only des résultats instlatx64. La conclusion est donc vraiment que tous les movsb, movsd, movsq effectuent approximativement de la même manière sur toutes les plateformes Intel récentes. La conclusion la plus intéressante est probablement "ne pas utiliser movsw". Vous ne comparez pas à une boucle explicite d'instructions mov (y compris les mouvements de 16 octets sur les plateformes 64 bits, qui sont garantis d'être disponibles), ce qui sera probablement plus rapide dans de nombreux cas. Vous ne montrez pas ce qui se passe sur les plateformes AMD, ni lorsque la taille dépasse la taille L1.

2 votes

Enfin, vous devez noter que rien d'autre que rep movsb n'implémente réellement memcpy (et aucun d'entre eux n'implémente memmove), donc vous avez besoin de code supplémentaire pour les autres variantes. Cela est seulement susceptible d'avoir de l'importance pour les petites tailles.

9voto

David Wohlferd Points 650

Vous dites que vous voulez :

une réponse qui montre quand ERMSB est utile

Mais je ne suis pas sûr que cela signifie ce que vous pensez. En regardant la documentation 3.7.6.1 à laquelle vous faites référence, il dit explicitement :

implémenter memcpy en utilisant ERMSB pourrait ne pas atteindre le même niveau de débit que l'utilisation des alternatives AVX 256 bits ou 128 bits, en fonction de la longueur et des facteurs d'alignement.

Donc, juste parce que CPUID indique le support pour ERMSB, cela ne garantit pas que REP MOVSB sera la manière la plus rapide de copier la mémoire. Cela veut juste dire que cela ne sera pas aussi mauvais que cela a été avec certains anciens CPU.

Cependant, juste parce qu'il peut y avoir des alternatives qui, dans certaines conditions, peuvent fonctionner plus rapidement, ne signifie pas que REP MOVSB est inutile. Maintenant que les pénalités de performance que cette instruction avait l'habitude d'encourir ont disparu, c'est potentiellement de nouveau une instruction utile.

Rappelez-vous, c'est un tout petit bout de code (2 octets !) comparé à certaines des routines memcpy plus complexes que j'ai vues. Puisque charger et exécuter de gros morceaux de code a aussi une pénalité (en éjectant une partie de votre autre code du cache du CPU), parfois le 'bénéfice' d'AVX, entre autres, sera compensé par l'impact qu'il a sur le reste de votre code. Cela dépend de ce que vous faites.

Vous demandez également :

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

Il ne sera pas possible de "faire quelque chose" pour rendre REP MOVSB plus rapide. Il fait ce qu'il fait.

Si vous voulez les vitesses plus élevées que vous voyez avec memcpy, vous pouvez trouver le code source. Il est quelque part là-bas. Ou vous pouvez le suivre depuis un débogueur et voir les chemins de code réels pris. Je m'attends à ce qu'il utilise certaines de ces instructions AVX pour travailler avec 128 ou 256 bits à la fois.

Ou vous pouvez simplement... Eh bien, vous nous avez demandé de ne pas le dire.

0 votes

J'ai testé REP MOVSB pour des tailles dans le cache L3 et en effet elle est compétitive avec une solution SSE/AVX. Mais je n'ai pas encore trouvé qu'elle soit clairement meilleure. Pour des tailles plus grandes que le cache L3, les magasins non-temporels gagnent encore largement. Votre remarque sur la taille du code est intéressante et mérite d'être prise en compte. Je ne connais pas grand chose sur le microcode. REP MOVSB est implémenté avec du microcode, donc même s'il n'utilise pas beaucoup du cache de code et compte seulement comme une instruction, il peut quand même utiliser beaucoup des ports et/ou micro-opérations.

0 votes

"n'ai pas encore trouvé qu'il soit clairement meilleur." Meilleur que quoi? "Amélioré" n'est pas la même chose que "Optimal". Je n'ai vu aucun endroit promettant que ce serait le meilleur performant. Je ne crois pas que c'est ce que le drapeau CPU est censé transmettre. C'est mieux que ce n'était sur des plateformes où il subissait une pénalité (même sur une boucle movq/cmp). "Taille du code" n'est pas toujours facile à voir. Tout comme la mémoire stockée dans les lignes de cache qui est échangée dans et hors de l'UCP, le code l'est aussi. Charger un vieux memcpy de grande taille signifie que certaines parties de votre code seront évincées.

0 votes

Voir la fin de ma question où je cite un commentaire affirmant que ERMSB devrait être meilleur que les magasins non temporels même pour des tailles importantes.

9voto

Nominal Animal Points 7207

Ceci n'est pas une réponse aux questions posées, seulement mes résultats (et conclusions personnelles) en essayant de trouver une réponse.

En résumé : GCC optimise déjà memset()/memmove()/memcpy() (voir par exemple gcc/config/i386/i386.c:expand_set_or_movmem_via_rep() dans les sources de GCC; cherchez également stringop_algs dans le même fichier pour voir les variantes dépendantes de l'architecture). Il n'y a donc pas de raison de s'attendre à des gains massifs en utilisant votre propre variante avec GCC (sauf si vous oubliez des choses importantes comme les attributs d'alignement pour vos données alignées, ou si vous n'activez pas suffisamment d'optimisations spécifiques comme -O2 -march= -mtune=). Si vous êtes d'accord, alors les réponses aux questions posées sont plus ou moins sans importance en pratique.

(Je souhaite seulement qu'il y ait un memrepeat(), l'opposé de memcpy() par rapport à memmove(), qui répéterait la partie initiale d'un tampon pour remplir l'intégralité du tampon.)


Je possède actuellement une machine Ivy Bridge en cours d'utilisation (ordinateur portable Core i5-6200U, noyau Linux 4.4.0 x86-64, avec erms dans les indicateurs de /proc/cpuinfo). Parce que je voulais savoir si je pouvais trouver un cas où une variante personnalisée de memcpy() basée sur rep movsb serait plus performante qu'un memcpy() simple, j'ai écrit un banc d'essai excessivement compliqué.

L'idée principale est que le programme principal alloue trois grandes zones de mémoire : original, current et correct, chacune exactement de la même taille et au moins alignée sur une page. Les opérations de copie sont regroupées en ensembles, chaque ensemble ayant des propriétés distinctes, comme tous les sources et les cibles étant alignés (à un certain nombre d'octets), ou toutes les longueurs étant dans la même plage. Chaque ensemble est décrit à l'aide d'un tableau de triplets src, dst, n, où tous les src à src+n-1 et dst à dst+n-1 sont complètement dans la zone current.

Un PRNG Xorshift* est utilisé pour initialiser original avec des données aléatoires. (Comme je l'ai averti précédemment, c'est excessivement compliqué, mais je voulais m'assurer de ne pas laisser de raccourcis faciles pour le compilateur.) La zone correct est obtenue en partant des données de original dans current, en appliquant tous les triplets de l'ensemble actuel, en utilisant memcpy() fourni par la bibliothèque C, et en copiant la zone current dans correct. Cela permet de vérifier que chaque fonction testée se comporte correctement.

Chaque ensemble d'opérations de copie est chronométré un grand nombre de fois en utilisant la même fonction, et la médiane de ces temps est utilisée pour la comparaison. (À mon avis, la médiane a le plus de sens en matière de test de performance, et fournit une sémantique sensée -- la fonction est au moins aussi rapide au moins la moitié du temps.)

Pour éviter les optimisations du compilateur, j'ai programmé le chargement des fonctions et des tests de manière dynamique, au moment de l'exécution. Les fonctions ont toutes la même forme, void fonction(void *, const void *, size_t) -- notez que contrairement à memcpy() et memmove(), elles ne retournent rien. Les tests (ensembles d'opérations de copie) sont générés dynamiquement par un appel de fonction (qui prend le pointeur de la zone current et sa taille comme paramètres, entre autres).

Malheureusement, je n'ai pas encore trouvé de cas où

static void rep_movsb(void *dst, const void *src, size_t n)
{
    __asm__ __volatile__ ( "rep movsb\n\t"
                         : "+D" (dst), "+S" (src), "+c" (n)
                         :
                         : "memory" );
}

serait plus efficace que

static void normal_memcpy(void *dst, const void *src, size_t n)
{
    memcpy(dst, src, n);
}

en utilisant gcc -Wall -O2 -march=ivybridge -mtune=ivybridge avec GCC 5.4.0 sur le portable Core i5-6200U mentionné exécutant un noyau Linux 4.4.0 64 bits. Copier des morceaux de 4096 octets alignés et de taille similaire s'en approche, cependant.

Cela signifie qu'au moins jusqu'à présent, je n'ai pas trouvé de cas où l'utilisation d'une variante de memcpy rep movsb aurait du sens. Cela ne signifie pas qu'il n'y a pas de cas approprié ; j'en n'ai tout simplement pas trouvé.

(À ce stade, le code est un désordre spaghetti dont je suis plus honteux que fier, donc je vais m'abstenir de publier les sources à moins que quelqu'un ne les demande. La description ci-dessus devrait être suffisante pour en rédiger une meilleure, cependant.)


Cela ne me surprend pas beaucoup. Le compilateur C peut déduire de nombreuses informations sur l'alignement des pointeurs opérandes, et si le nombre d'octets à copier est une constante de compilation ou un multiple d'une puissance de deux adaptée. Ces informations peuvent, et seront/devraient, être utilisées par le compilateur pour remplacer les fonctions memcpy()/memmove() de la bibliothèque C par les siennes.

GCC fait exactement cela (voir par exemple [gcc/config/i386/i386.c:expand_set_or_movmem_via_rep()](https://raw.githubusercontent.com/gcc-mirror/gcc/master/gcc/config/i386/i386.c:expand_set_or_movmem_via_rep()) dans les sources de GCC; cherchez également stringop_algs dans le même fichier pour voir les variantes dépendantes de l'architecture). En effet, memcpy()/memset()/memmove() a déjà été optimisé séparément pour plusieurs variantes de processeurs x86 ; cela me surprendrait beaucoup si les développeurs de GCC n'avaient pas déjà inclus le support erms.

GCC fournit plusieurs attributs de fonction que les développeurs peuvent utiliser pour garantir un code généré efficace. Par exemple, alloc_align(n) dit à GCC que la fonction renvoie une mémoire alignée sur au moins n octets. Une application ou une bibliothèque peut choisir quelle implémentation d'une fonction utiliser au moment de l'exécution, en créant une "fonction de résolution" (qui renvoie un pointeur de fonction), et en définissant la fonction en utilisant l'attribut ifunc(resolver).

Un des modèles les plus courants que j'utilise dans mon code pour cela est

some_type *pointer = __builtin_assume_aligned(ptr, alignment);

ptr est un pointeur, alignment est le nombre d'octets auxquels il est aligné ; GCC sait/al'assure donc que pointer est aligné sur alignment octets.

Un autre built-in utile, bien que beaucoup plus difficile à utiliser correctement, est __builtin_prefetch(). Pour maximiser la bande passante/efficacité globale, j'ai constaté que le fait de minimiser les latences dans chaque sous-opération donne les meilleurs résultats. (Pour copier des éléments dispersés vers un stockage temporaire consécutif, c'est difficile, car le prefetching implique généralement une ligne de cache complète ; si trop d'éléments sont prefetchés, la plupart de la mémoire cache est gaspillée en stockant des éléments inutilisés.)

0 votes

Le portable i5-6200U n'est pas Ivy Bridge. C'est Skylake. Ce serait intéressant de voir le tingybenchmark sur un système Ivy Bridge.

0 votes

@Zboson: Vrai true; merci de l'avoir souligné. Je ne sais pas d'où je tenais cette hypothèse; probablement sortie de nulle part. Ouch. Cela explique aussi mes résultats.

4voto

David Hoelzer Points 269

Il existe des moyens beaucoup plus efficaces de déplacer des données. De nos jours, l'implémentation de memcpy générera un code spécifique à l'architecture à partir du compilateur qui est optimisé en fonction de l'alignement de la mémoire des données et d'autres facteurs. Cela permet une meilleure utilisation des instructions de cache non temporelles et des registres XMM et autres dans le monde x86.

Lorsque vous codifiez en dur rep movsb, cela empêche l'utilisation d'intrinsics.

Par conséquent, pour quelque chose comme un memcpy, à moins que vous n'écriviez quelque chose qui sera lié à un matériel très spécifique et à moins que vous ne preniez le temps d'écrire une fonction memcpy hautement optimisée en langage d'assemblage (ou en utilisant des intrinsics de niveau C), il vaut mieux laisser le compilateur le résoudre pour vous.

2 votes

En réalité, avec rep movsb amélioré, l'utilisation de rep movsd est plus lente. Veuillez lire ce que cette fonctionnalité signifie avant d'écrire des réponses comme celle-ci.

0 votes

Hmm... Merci. J'ai retiré le commentaire rep movsd.

2 votes

J'ai discuté d'un memcpy personnalisé ici. Un commentaire dit "Notez que sur Ivybridge et Haswell, avec des tampons trop grands pour tenir dans le MLC, vous pouvez battre movntdqa en utilisant rep movsb ; movntdqa entraîne un RFO dans le LLC, rep movsb non." Je peux obtenir quelque chose d'aussi bon qu'un memcpy avec movntdqa. Ma question est comment puis-je faire aussi bien que cela ou mieux avec rep movsb?

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