web-dev-qa-db-fra.com

REP amélioré MOVSB ​​pour la mémoire

Je voudrais utiliser REP MOVSB ​​(ERMSB) amélioré pour obtenir une bande passante élevée pour un memcpy personnalisé.

ERMSB a été introduit avec la microarchitecture de Ivy Bridge. Reportez-vous à la section "Fonctionnement amélioré des fichiers REP MOVSB ​​et STOSB (ERMSB)" dans Manuel d’optimisation Intel si vous ne savez pas ce que est ERMSB.

Le seul moyen que je connaisse pour le faire directement consiste à assembler en ligne. 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;
}

Quand j'utilise ceci cependant, la bande passante est beaucoup moins qu'avec memcpy. __movsb obtient 15 Go/s et memcpy obtient 26 Go/s avec mon système i7-6700HQ (Skylake), Ubuntu 16.10, DDR4 @ 2400 MHz double canal 32 Go, GCC 6.2.

Pourquoi la bande passante est-elle si inférieure 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 <stdlib.h>
#include <string.h>
#include <stdio.h>
#include <stddef.h>
#include <omp.h>
#include <x86intrin.h>

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 GB/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 GB/s\n", dtime, 2.0*10*1E-9*n/dtime);  
}

La raison pour laquelle je suis intéressé par rep movsb est basé sur ces commentaires

Notez que sur Ivybridge et Haswell, avec des tampons trop grands pour tenir dans MLC, vous pouvez battre movntdqa en utilisant rep movsb; movntdqa engage un RFO dans la LLC, le représentant movsb ne le fait pas ... rep movsb est nettement plus rapide que le movntdqa lors de la diffusion en mémoire sur Ivybridge et Haswell (mais sachez qu'avant Ivybridge, il est lent!)

Que manque-t-il/sous-optimal dans cette implémentation memcpy?


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

 C copy backwards                                     :   7910.6 MB/s (1.4%)
 C copy backwards (32 byte blocks)                    :   7696.6 MB/s (0.9%)
 C copy backwards (64 byte blocks)                    :   7679.5 MB/s (0.7%)
 C copy                                               :   8811.0 MB/s (1.2%)
 C copy prefetched (32 bytes step)                    :   9328.4 MB/s (0.5%)
 C copy prefetched (64 bytes step)                    :   9355.1 MB/s (0.6%)
 C 2-pass copy                                        :   6474.3 MB/s (1.3%)
 C 2-pass copy prefetched (32 bytes step)             :   7072.9 MB/s (1.2%)
 C 2-pass copy prefetched (64 bytes step)             :   7065.2 MB/s (0.8%)
 C fill                                               :  14426.0 MB/s (1.5%)
 C fill (shuffle within 16 byte blocks)               :  14198.0 MB/s (1.1%)
 C fill (shuffle within 32 byte blocks)               :  14422.0 MB/s (1.7%)
 C fill (shuffle within 64 byte blocks)               :  14178.3 MB/s (1.0%)
 ---
 standard memcpy                                      :  12784.4 MB/s (1.9%)
 standard memset                                      :  30630.3 MB/s (1.1%)
 ---
 MOVSB copy                                           :   8712.0 MB/s (2.0%)
 MOVSD copy                                           :   8712.7 MB/s (1.9%)
 SSE2 copy                                            :   8952.2 MB/s (0.7%)
 SSE2 nontemporal copy                                :  12538.2 MB/s (0.8%)
 SSE2 copy prefetched (32 bytes step)                 :   9553.6 MB/s (0.8%)
 SSE2 copy prefetched (64 bytes step)                 :   9458.5 MB/s (0.5%)
 SSE2 nontemporal copy prefetched (32 bytes step)     :  13103.2 MB/s (0.7%)
 SSE2 nontemporal copy prefetched (64 bytes step)     :  13179.1 MB/s (0.9%)
 SSE2 2-pass copy                                     :   7250.6 MB/s (0.7%)
 SSE2 2-pass copy prefetched (32 bytes step)          :   7437.8 MB/s (0.6%)
 SSE2 2-pass copy prefetched (64 bytes step)          :   7498.2 MB/s (0.9%)
 SSE2 2-pass nontemporal copy                         :   3776.6 MB/s (1.4%)
 SSE2 fill                                            :  14701.3 MB/s (1.6%)
 SSE2 nontemporal fill                                :  34188.3 MB/s (0.8%)

Notez que sur mon système SSE2 copy prefetched est aussi plus rapide que MOVSB copy.


Dans mes tests originaux, je n'ai pas désactivé le turbo. J'ai désactivé turbo et testé à nouveau et 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 un temps d'inactivité, une charge de 1 et un 4 charge de base. J'ai exécuté la multiplication de matrice dense MKL d'Intel pour créer une charge et définir le nombre de threads à l'aide de OMP_SET_NUM_THREADS. Voici un tableau des résultats (nombres en GHz).

              SpeedStep     idle      1 core    4 core
powersave     OFF           0.8       2.6       2.6
performance   OFF           2.6       2.6       2.6
powersave     ON            0.8       3.5       3.1
performance   ON            3.5       3.5       3.1

Cela montre qu'avec powersave même avec SpeedStep désactivé, le processeur est toujours cadencé à la fréquence d'inactivité de 0.8 GHz. Ce n'est qu'avec performance sans SpeedStep que le processeur fonctionne à une fréquence constante.

J'ai utilisé par exemple Sudo cpufreq-set -r performance (car cpufreq-set donnait des résultats étranges) pour modifier les paramètres d'alimentation. Cela rallume le turbo, donc je devais le désactiver après.

54
Z boson

C’est un sujet qui me tient à cœur et qui fait l’objet de récentes enquêtes. J’examinerai donc le sujet sous plusieurs angles: histoire, notes techniques (pour la plupart académiques), résultats de tests sur ma boîte et enfin tentative de réponse à votre question. de quand et où rep movsb pourrait avoir un sens.

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

Histoire et conseil officiel

L’historique des performances des instructions de copie rapide de chaînes a été plutôt complexe, c’est-à-dire des périodes de performances stagnantes alternant avec de grandes mises à niveau qui les ont alignées ou même plus rapidement que les approches concurrentes. Par exemple, les performances de Nehalem ont fortement augmenté (ciblant principalement les coûts indirects de démarrage), puis Ivy Bridge (la plupart ciblant le débit total pour les gros exemplaires). Vous pouvez trouver un aperçu d'une décennie sur les difficultés d'implémentation des instructions rep movs D'un ingénieur Intel dans ce fil .

Par exemple, dans les guides précédant l'introduction d'Ivy Bridge, le conseil typique est de les éviter ou de les utiliser avec précaution.1.

Le guide actuel (de juin 2016) contient divers conseils déroutants et quelque peu incohérents, tels que2:

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). Par exemple, MOVSB ​​/ STOSB avec le préfixe REP doit être utilisé avec une valeur de compteur inférieure ou égale à trois pour une performance optimale.

Donc, pour des copies de 3 octets ou moins? Vous n'avez pas besoin d'un préfixe rep pour cela, car avec une latence de démarrage déclarée de ~ 9 cycles, vous êtes certainement certainement mieux avec un simple DWORD ou QWORD mov avec un peu de bricolage pour masquer les octets inutilisés (ou peut-être avec 2 octets explicites, Word movs si vous savez que la taille est exactement trois).

Ils continuent en disant:

Les instructions String MOVE/STORE ont plusieurs granularités de données. Pour un transfert de données efficace, des granularités de données plus grandes sont préférables. Cela signifie qu'une meilleure efficacité peut être obtenue en décomposant une valeur de compteur arbitraire en un nombre de mots doubles plus des déplacements d'un octet 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 les variantes movd ou movq pour les copies volumineuses.

En général, cette section (3.7.5) du guide actuel contient une combinaison de conseils raisonnables et de conseils obsolètes. Cela est courant dans les manuels Intel, car ils sont mis à jour de manière incrémentielle pour chaque architecture (et sont censés couvrir près de deux décennies d'architectures, même dans le manuel actuel), et les anciennes sections ne sont souvent pas mises à jour pour remplacer ou émettre des conseils conditionnels. cela ne s'applique pas à l'architecture actuelle.

Ils couvrent ensuite explicitement ERMSB dans la section 3.7.6.

Je ne reviendrai pas de manière exhaustive sur les conseils restants, mais je résumerai les bonnes parties dans la section "Pourquoi l’utiliser" ci-dessous.

Parmi les autres affirmations importantes de ce guide, citons le fait que, sur Haswell, rep movsb A été amélioré pour pouvoir utiliser des opérations 256 bits en interne.

Considérations techniques

Ceci est juste un résumé rapide des avantages et inconvénients sous-jacents que les instructions rep ont du point de vue de la mise en oeuvre .

Avantages pour rep movs

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

    • Éviter la demande RFO quand il sait que toute la ligne de cache sera écrasée.
    • Envoi de demandes de prélecture immédiatement et exactement. La pré-extraction matérielle détecte bien les modèles de type memcpy-, mais il faut encore quelques lectures pour démarrer et "surcharger" 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 effectuer une prélecture exacte.
  2. Apparemment, il n'y a aucune garantie de commande parmi les magasins dans les3 un seul rep movs qui peut aider à simplifier le trafic de cohérence et simplement d'autres aspects du déplacement de bloc, par rapport à de simples instructions mov qui doivent obéir à un ordre de mémoire assez strict4.

  3. En principe, l'instruction rep movs Pourrait tirer parti 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 le ISA expose5 et rep movs pourrait utiliser cela en interne.

Désavantages

  1. rep movsb Doit implémenter 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 attendu. Sur les implémentations actuelles, cela affecte principalement le temps système nécessaire au démarrage, mais probablement pas le débit des blocs volumineux. De la même manière, rep movsb Doit prendre en charge les copies granulaires sur octets même si vous l'utilisez réellement pour copier des blocs volumineux qui sont un multiple d'une puissance importante de 2.

  2. Le logiciel peut contenir des informations sur l'alignement, la taille de la copie et un alias possible qui ne peuvent pas être communiquées au matériel si vous utilisez rep movsb. Les compilateurs peuvent souvent déterminer l'alignement des blocs de mémoire6 et donc peut éviter une grande partie du travail de démarrage que rep movs doit faire sur every invocation.

Résultats de test

Voici les résultats des tests pour différentes méthodes de copie de tinymembench sur mon i7-6700HQ à 2,6 GHz (dommage que je dispose du même processeur, nous ne sommes pas obtenir 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 points à retenir:

  • Les méthodes rep movs Sont plus rapides que toutes les autres méthodes qui ne sont pas "non temporelles"7, et considérablement plus rapide que les approches "C" qui copient 8 octets à la fois.
  • Les méthodes "non temporelles" sont plus rapides, d’environ 26% par rapport à rep movs - mais c’est un delta beaucoup plus petit que celui que vous avez signalé (26 Go/s contre 15 Go/s = ~ 73 %).
  • Si vous n'utilisez pas de magasins non temporels, utiliser des copies de 8 octets à partir de C équivaut à peu près à un chargement/magasin de 128 bits de large SSE. En effet, une bonne boucle de copie peut générer suffisamment de pression sur la mémoire pour saturer la bande passante (par exemple, 2,6 GHz * 1 mémoire/cycle * 8 octets = 26 Go/s pour les magasins).
  • Il n’existe pas d’algorithme 256 bits explicite dans tinymembench (sauf probablement le "standard" memcpy), mais cela n’a probablement aucune importance en raison de la remarque ci-dessus.
  • Le débit accru des approches de stockage non temporelles par rapport aux solutions temporelles est d’environ 1,45, ce qui est très proche de celui attendu de 1,5 si NT élimine 1 transfert sur 3 (c'est-à-dire 1 lecture, 1 écriture pour NT vs 2). lit, 1 écriture). Les approches rep movs Se situent au milieu.
  • La combinaison d'une latence mémoire relativement faible et d'une bande passante 2 canaux modeste signifie que cette puce peut 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. Cela est intéressant car ERMSB ne cible explicitement que movsb et les tests antérieurs sur des arcs précédents avec ERMSB montrent que movsb effectue beaucoup plus rapidement que movsd. Ceci est surtout académique puisque movsb est plus général que movsd de toute façon.

Haswell

En regardant les résultats de Haswell aimablement fournis par iwillnotexist dans les commentaires, nous observons 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%)

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

Quand devriez-vous utiliser rep movs?

Enfin, essayez de répondre à votre question: quand ou pourquoi devriez-vous l’utiliser? Il s’inspire de ce qui précède et introduit quelques nouvelles idées. Malheureusement, il n’existe pas de réponse simple: vous devrez faire l’échange de plusieurs facteurs, dont certains que vous ne pouvez probablement même pas connaître avec précision, tels que les développements futurs.

Une note indiquant que l’alternative à rep movsb Peut être la libc optimisée memcpy (y compris les copies insérées par le compilateur) ou une version memcpy roulée à la main. Certains des avantages ci-dessous ne sont valables que par rapport à l’une ou l’autre de ces solutions (par exemple, la "simplicité" aide par rapport à une version roulée à la main, mais pas à la memcpy intégrée), mais certains s’appliquent à tous les deux.

Restrictions sur les instructions disponibles

Dans certains environnements, certaines instructions ou certains registres sont restreints. 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 variantes memcpy optimisées ne peuvent pas être utilisées car elles reposent sur des registres SSE ou AVX, et une copie 64 bits à la mov est utilisée x86. Pour ces plates-formes, l’utilisation de rep movsb Permet la plupart des performances d’un memcpy optimisé sans rompre la restriction du code SIMD.

Un exemple plus général pourrait être le code qui doit cibler de nombreuses générations de matériel et qui n’utilise pas la répartition spécifique au matériel (par exemple, en utilisant cpuid). Ici, vous pourriez être obligé d'utiliser uniquement des jeux d'instructions plus anciens, ce qui exclut tout AVX, etc. rep movsb Pourrait être une bonne approche car il permet un accès "caché" à des charges et des magasins plus larges sans utiliser de nouvelles instructions. Si vous ciblez du matériel pré-ERMSB, il vous faudra voir si les performances de rep movsb Sont acceptables, mais ...

Vérification future

Un bel aspect de rep movsb Est qu’il peut, en théorie == tirer parti des améliorations architecturales apportées aux architectures futures, sans modification de la source, ce que ne peuvent pas faire des déplacements explicites. Par exemple, lorsque des chemins de données 256 bits ont été introduits, rep movsb A pu en tirer parti (comme l'affirme Intel) sans aucune modification nécessaire du logiciel. Les logiciels utilisant des mouvements 128 bits (ce qui était optimal avant Haswell) devraient être modifiés et recompilés.

Il s'agit donc à la fois d'un avantage en termes de maintenance logicielle (pas besoin de changer de source) et d'un avantage pour les fichiers binaires existants (pas besoin de déployer de nouveaux fichiers binaires pour tirer parti de l'amélioration).

L’importance de cet aspect dépend de votre modèle de maintenance (par exemple, la fréquence à laquelle de nouveaux fichiers binaires sont déployés dans la pratique) et il est très difficile de juger de la vitesse probable de ces instructions dans l’avenir. Au moins Intel est-il en quelque sorte un guide dans cette direction, en s'engageant à au moins raisonnable la performance future ( 15.3.3.6 ):

REP MOVSB ​​et REP STOSB continueront à obtenir de bons résultats sur les futurs processeurs.

Chevauchement avec des travaux ultérieurs

Cet avantage n'apparaîtra pas dans un simple repère memcpy bien sûr, qui, par définition, ne doit pas se chevaucher par la suite, de sorte que l'ampleur de l'avantage devrait être soigneusement mesurée dans un monde réel. scénario. Tirer le maximum d’avantages pourrait nécessiter une réorganisation du code entourant la memcpy.

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

Lorsque le nombre est au moins égal à mille octets ou plus, l’utilisation de REP amélioré MOVSB ​​/ STOSB peut offrir un autre avantage permettant d’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 128 instances de fonctionnement du stockage sur 32 octets avec VMOVDQA, avant que les séquences d'instructions non consommatrices ne puissent être annulées.

• Une instance de REP STOSB amélioré avec ECX = 4096 est décodée en tant que long flux micro-op fourni par le matériel, mais se retire en une seule instruction. De nombreuses opérations store_data doivent être terminées avant que le résultat de memset () puisse être utilisé. Etant donné que l'achèvement de l'opération de stockage des données est découplé de la suppression de l'ordre du programme, une partie importante du flux de code non consommant peut être traitée lors de l'émission/exécution et de la suppression, essentiellement sans frais si la séquence non consommatrice n'est pas concurrente. pour les ressources du tampon de magasin.

Donc, Intel dit qu'après tout, le code après que rep movsb Ait été émis, mais que de nombreux magasins sont encore en vol et que le rep movsb Dans son ensemble ne s'est pas encore retiré, il faut suivre les instructions peuvent faire plus de progrès dans la machinerie en panne qu’elles ne le pourraient si ce code venait après une boucle de copie.

Les uops d'un chargement explicite et d'une boucle de stockage doivent tous être retirés séparément dans l'ordre du programme. Cela doit arriver pour permettre à l’Ordre de suivre les ordres.

Il ne semble pas y avoir beaucoup d’informations détaillées sur la manière dont fonctionne une très longue instruction microcodée telle que rep movsb. Nous ne savons pas exactement comment les branches de micro-code demandent un flux différent d'ops du séquenceur à microcode, ni comment les uops se retirent. Si les oups individuels ne doivent pas prendre leur retraite séparément, peut-être que l'instruction complète ne prend qu'une place dans le ROB.

Lorsque le frontal qui alimente la machine OoO voit une instruction rep movsb Dans le cache uop, il active le séquenceur de microcodes ROM (MS-ROM) pour envoyer des uops de microcode dans la file qui les alimente. l'étape d'émission/renommer. Il n’est probablement pas possible pour d’autres uops de se mêler à cela et de lancer/exécuter8 alors que rep movsb est toujours en cours d'émission, mais les instructions suivantes peuvent être extraites/décodées et émises juste après le dernier rep movsb uop, 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 suivant ne dépend pas du résultat de la memcpy (ce qui n'est pas inhabituel).

Maintenant, la taille de cet avantage est limitée: vous pouvez tout au plus exécuter N instructions (en fait, uops) au-delà de l'instruction lente rep movsb, Auquel cas vous allez vous caler, où N est le Taille ROB . Avec des tailles de ROB actuelles d'environ ~ 200 (192 sur Haswell, 224 sur Skylake), vous bénéficiez d'un avantage maximal d'environ 200 cycles de travail libre pour le code suivant avec un IPC de 1. Vous pouvez copier quelque part dans 200 cycles. environ 800 octets à 10 Go/s. Ainsi, pour des copies de cette taille, vous pouvez obtenir un travail gratuit proche du coût de la copie (de manière à rendre la copie gratuite).

Toutefois, à mesure que les tailles de copie deviennent beaucoup plus grandes, son importance relative diminue rapidement (par exemple, si vous copiez 80 ko au lieu de cela, le travail gratuit ne représente que 1% du coût de la copie). Néanmoins, c’est assez intéressant pour les copies de taille modeste.

Les boucles de copie ne bloquent pas non plus totalement l'exécution d'instructions ultérieures. Intel n'entre pas dans les détails sur l'ampleur de l'avantage, ni sur le type de copie ou de code environnant qui présente le plus d'avantages. (Destination ou source chaude ou froide, code ILP élevé ou code ILP élevé de latence élevée après).

Taille du code

La taille du code exécuté (quelques octets) est microscopique par rapport à une routine optimisée typique memcpy. Si les performances sont limitées par les échecs i-cache (y compris le cache uop), la taille réduite du code peut présenter des avantages.

Là encore, nous pouvons limiter l’ampleur de cet avantage en fonction de la taille de la copie. Je ne vais pas régler le problème numériquement, mais l'intuition est que réduire la taille du code dynamique de B octets peut économiser au maximum C * B Cache-misses, pour une constante C. Every call à memcpy encourt le coût (ou l'avantage) de la mémoire cache, une fois, mais l'avantage d'un débit plus élevé varie en fonction du nombre d'octets copiés. Donc, pour les transferts volumineux, un débit plus élevé dominera les effets de cache.

Encore une fois, ce n'est pas quelque chose qui va apparaître dans un repère simple, où la boucle entière va sans doute tenir dans le cache uop. Vous aurez besoin d'un test sur site réel pour évaluer cet effet.

Optimisation spécifique à l'architecture

Vous avez signalé que sur votre matériel, rep movsb Était considérablement plus lent que la plate-forme memcpy. Cependant, même dans ce cas, des résultats opposés ont été signalés sur du matériel antérieur (comme Ivy Bridge).

C’est tout à fait plausible, car il semble que les opérations de déplacement de chaîne subissent de l’amour périodiquement - mais pas à chaque génération, il est donc possible qu’elle soit plus rapide ou du moins liée (à ce stade, elle peut gagner grâce à d’autres avantages) aux architectures où elle a été construite. mis à jour, pour prendre du retard dans le matériel suivant.

Citant Andy Glew, qui devrait en savoir une ou deux choses à ce sujet après l’avoir appliqué sur le P6:

la grande faiblesse des chaînes rapides dans le microcode était que le [...] microcode se désaccordait avec chaque génération, devenant de plus en plus lent jusqu'à ce que quelqu'un se débrouille pour le réparer. Tout comme une bibliothèque, la copie est désaccordée. Je suppose qu'il est possible que l'une des opportunités manquées ait été d'utiliser des charges et des magasins 128 bits lorsqu'ils sont devenus disponibles, etc.

Dans ce cas, on peut considérer qu’il s’agit simplement d’une autre optimisation "spécifique à la plate-forme" à appliquer dans les routines typiques de chaque astuce du livre memcpy que vous trouverez dans les bibliothèques standard et les compilateurs JIT: utiliser sur des architectures où c'est mieux. Ceci est facile pour les fichiers compilés JIT ou AOT, mais pour les binaires compilés statiquement, cela nécessite une répartition spécifique à la plate-forme, mais cela existe déjà (parfois au moment du lien), ou l'argument mtune peut être utilisé pour prendre une décision statique.

Simplicité

Même sur Skylake, où il semble avoir pris du retard par rapport aux techniques non temporelles les plus rapides, elle est toujours plus rapide que la plupart des approches et very simple . Cela signifie moins de temps pour la validation, moins de bugs mystères, moins de temps pour mettre au point et mettre à jour une implémentation monstre memcpy (ou, inversement, moins de dépendance des caprices des implémenteurs de bibliothèques standard si vous vous en remettez à cela).

Plateformes liées à la latence

Algorithmes liés au débit de mémoire9 peut effectivement fonctionner dans deux régimes généraux principaux: la bande passante DRAM liée ou la concurrence/latence.

Le premier mode est celui avec lequel vous êtes probablement familier: le sous-système DRAM a une certaine bande passante théorique que vous pouvez calculer assez facilement en fonction du nombre de canaux, du débit de données/largeur 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 Go/s, identique à indiquée sur ARK .

Vous ne maintiendrez pas plus que ce taux de DRAM (et généralement un peu moins en raison de diverses inefficiences) ajouté à tous les cœurs du socket (c’est-à-dire qu’il s’agit d’une limite globale pour les systèmes à socket unique).

L'autre limite est imposée par le nombre de demandes simultanées qu'un cœur peut réellement envoyer au sous-système de mémoire. Imaginons qu'un cœur ne puisse avoir qu'une seule demande en cours à la fois, pour une ligne de cache de 64 octets; lorsque la demande sera terminée, vous pourrez en émettre une autre. Supposons également une latence mémoire très rapide de 50 ns. Ensuite, malgré la grande bande passante DRAM de 34,1 Go/s, vous n’obtenez en réalité 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 plusieurs demandes à la fois, mais pas un nombre illimité. Il est généralement admis qu'il n'y a que 10 tampons de remplissage de ligne par cœur entre le N1 et le reste de la hiérarchie de la mémoire, et peut-être environ 16 tampons de remplissage entre L2 et DRAM. La pré-extraction est en concurrence pour les mêmes ressources, mais aide au moins à réduire la latence effective. Pour plus de détails, jetez un œil à l’un des meilleurs messages . La bande passante a écrit sur le sujet , principalement sur les forums Intel.

Cependant, most == les CPU récents sont limités par le facteur this , et non par la bande passante RAM. Ils atteignent généralement 12 - 20 Go/s par cœur, tandis que la bande passante RAM peut être supérieure à 50 Go/s (sur un système à 4 canaux). Seuls quelques cœurs "clients" récents de la génération 2 canaux, qui semblent avoir un meilleur uncore, plusieurs tampons de ligne peuvent atteindre la limite de DRAM sur un seul cœur, et nos puces Skylake semblent en faire partie.

Maintenant, bien sûr, il y a une raison pour laquelle Intel a conçu des systèmes avec une bande passante DRAM de 50 Go/s, tout en ne devant supporter que moins de 20 Go/s par cœur en raison de limites de concurrence: la première limite concerne l'ensemble des sockets et la dernière est par cœur. Ainsi, chaque cœur d’un système à 8 cœurs peut traiter des demandes d’une valeur de 20 Go/s, auquel cas elles seront à nouveau limitées en DRAM.

Pourquoi je continue à parler de ça? Parce que la meilleure implémentation de la memcpy dépend souvent du régime dans lequel vous opérez. Une fois que vous êtes DRAM BW limité (comme le sont apparemment nos puces, mais la plupart ne sont pas sur un seul cœur), en utilisant des écritures non temporelles devient très important car il enregistre la lecture pour la propriété qui gaspille normalement 1/3 de votre bande passante. Vous voyez cela exactement dans les résultats du test ci-dessus: les implémentations memcpy qui don't utilisent les magasins NT perdent 1/3 de leur bande passante.

Cependant, si vous êtes limité par la simultanéité, la situation est égale et parfois inversée. Vous avez de la bande passante DRAM à épargner. Les magasins NT n’aident donc pas et ils peuvent même être blessés, car ils risquent d’augmenter le temps de latence car le temps de transfert pour le tampon de ligne peut être plus long L2), puis le magasin se termine en LLC pour une latence inférieure effective. Enfin, server == les disques ont tendance à avoir des magasins NT beaucoup plus lents que les clients (et une bande passante élevée), ce qui accentue cet effet.

Ainsi, sur d'autres plates-formes, vous constaterez peut-être que les magasins NT sont moins utiles (du moins lorsque vous vous souciez des performances mono-threadées) et que rep movsb Gagne où (si vous obtenez le meilleur des deux mondes).

Vraiment, ce dernier élément est un appel à la plupart des tests. Je sais que les magasins NT perdent leur avantage apparent pour les tests mono-thread sur la plupart des archs (y compris les archs de serveur actuels), mais je ne sais pas comment rep movsb Sera relativement performant ...

Les références

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

enquête comp.Arch de rep movsb par rapport aux alternatives. Beaucoup de bonnes notes sur la prédiction de branche, et une implémentation de l’approche que j’ai souvent suggérée pour les petits blocs: utiliser le chevauchement de la première et/ou de la dernière lecture/écriture plutôt que d’essayer d’écrire exactement le nombre d’octets requis (par exemple, toutes les copies de 9 à 16 octets sous forme de deux copies de 8 octets pouvant se chevaucher dans un maximum de 7 octets).


1 L'intention est vraisemblablement de le 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 la même instruction: une fois terminé, le bloc de magasins apparaît toujours comme étant ordonné par rapport aux magasins précédents et suivants. Le code peut donc voir les magasins du rep movs Dans le désordre les uns par rapport aux autres mais pas par rapport aux magasins antérieurs ou suivants (et ce dernier vous garantit généralement avoir besoin). Ce ne sera un problème que si vous utilisez la fin de la destination de la copie comme indicateur de synchronisation, au lieu d'un magasin séparé.

4 Notez que les magasins discrets non temporels évitent également la plupart des exigences de commande, bien que, dans la pratique, rep movs Ait encore plus de liberté puisqu'il existe encore des contraintes de commande sur les magasins WC/NT.

5 Ceci était courant dans la dernière partie de l'ère 32 bits, où de nombreuses puces disposaient de chemins de données 64 bits (par exemple, pour prendre en charge les FPU prenant en charge le type double 64 bits). Aujourd'hui, les puces "stérilisées" telles que les marques Pentium ou Celeron ont désactivé AVX, mais le microcode rep movs Peut toujours utiliser des charges/magasins de 256b.

6 Par exemple, en raison de règles d'alignement de langage, d'attributs ou d'opérateurs d'alignement, de règles de repliement du spectre 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 pas être déterminé, ils peuvent au moins être en mesure de lever les vérifications d'alignement des boucles ou d'éliminer les vérifications redondantes.

7 Je suppose que "standard" memcpy choisit une approche non temporelle, ce qui est très probable pour cette taille de mémoire tampon.

8 Cela n’est pas forcément évident, car il se pourrait que le flux uop généré par le rep movsb Monopolise tout simplement l’envoi et ressemble alors beaucoup à la casse explicite mov. Il semble que cela ne fonctionne pas comme ça cependant - les uops des instructions suivantes peuvent se mêler aux uops du microcodé rep movsb.

9 C’est-à-dire ceux qui peuvent émettre un grand nombre de demandes de mémoire indépendantes et saturer ainsi la bande passante DRAM vers cœur disponible, dont memcpy serait un enfant poster (et se rapporterait à des charges liées à la latence pure telles que pointeur chassant).

68
BeeOnRope

REP MOVSB ​​amélioré (Ivy Bridge et ultérieur)

La microarchitecture de Ivy Bridge (processeurs commercialisés en 2012 et 2013) a introduit la version améliorée de MOVSB ​​ (nous devons toujours vérifier le bit correspondant) et nous a permis de copier rapidement de la mémoire. .

Les versions les moins chères des processeurs ultérieurs - Kaby Lake Celeron et Pentium, commercialisées en 2017, ne disposent pas d’AVX qui auraient pu être utilisés pour la copie rapide en mémoire, mais possèdent toujours la version améliorée du MOVSB.

REP MOVSB ​​(ERMSB) n’est plus rapide que la copie AVX ou la copie de registre à usage général si la taille du bloc est d’au moins 256 octets. Pour les blocs inférieurs à 64 octets, il est BEAUCOUP plus lent, car le démarrage interne dans ERMSB est élevé - environ 35 cycles.

Voir le Manuel d’optimisation d’Intel, section 3.7.6 Amélioration du fonctionnement des fichiers REP MOVSB ​​et STOSB (ERMSB) http://www.intel.com/content/dam/www/public/us/en/documents/manuals/ 64-ia-32-architectures-optimization-manual.pdf

  • le coût de démarrage est de 35 cycles;
  • les adresses source et de destination doivent être alignées sur une limite 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 en avant (CLD).

Comme je l’ai dit plus tôt, REP MOVSB ​​commence à surpasser les autres méthodes lorsque la longueur est d’au moins 256 octets, mais pour que l’avantage soit clair par rapport à la copie AVX, la longueur doit dépasser 2048 octets.

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

  • si le tampon source n'est pas aligné, l'impact sur l'implémentation d'ERMSB par rapport à AVX 128 bits est similaire.
  • si le tampon de destination n'est pas aligné, l'impact sur l'implémentation d'ERMSB peut être une dégradation de 25%, tandis qu'une implémentation de memcpy en AVX 128 bits ne peut se dégrader que de 5%, par rapport au scénario aligné sur 16 octets.

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

REP MOVSB ​​memcpy ():

 - 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 ... memcpy ():

 - 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 ​​est plus lent qu'une simple copie MOV RAX dans une boucle (non déroulée). L'implémentation ERMSB commence à surpasser la boucle MOV RAX à partir de blocs de 256 octets.

REP MOVS normal (non amélioré) sur Nehalem et plus tard

Étonnamment, les architectures précédentes (Nehalem et ultérieures), qui n’avaient pas encore amélioré REP MOVB, avaient une implémentation assez rapide de REP MOVSD/MOVSQ (mais pas de REP MOVSB ​​/ MOVSW) pour les gros blocs, mais pas assez pour surdimensionner le cache L1.

Le Manuel d’optimisation Intel (2.5.6 REP String Enhancement) fournit les informations suivantes sur la microarchitecture Nehalem - Intel Core i5, i7 et Xeon publiées en 2009 et 2010.

REP MOVSB

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

  • chaîne minuscule (ECX <4): la latence de REP MOVSB ​​est de 9 cycles;
  • petite chaîne (ECX se situe 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 de 50 cycles.

Ma conclusion: REP MOVSB ​​est presque inutile sur Nehalem.

MOVSW/MOVSD/MOVSQ

Extrait du manuel d'optimisation Intel (amélioration de la chaîne de caractères 2.5.6 REP):

  • Chaîne courte (ECX <= 12): la latence de REP MOVSW/MOVSD/MOVSQ est d’environ 20 cycles.
  • Chaîne rapide (ECX> = 76: à l'exclusion de REP MOVSB): la mise en œuvre du processeur fournit une optimisation matérielle en déplaçant autant de données que possible sur 16 octets. La latence de la latence de chaîne REP varie si l'une des transformations de données sur 16 octets s'étend sur la limite de la ligne de cache: = sans division: la latence correspond à un coût de démarrage d'environ 40 cycles et à chaque cycle de 64 octets, 4 cycles. = Fractionnement du cache: la latence représente un coût de démarrage d’environ 35 cycles et chaque octet de données ajoute 6 cycles.
  • Longueur de chaîne intermédiaire: la latence de 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 dans Word/dword/qword.

Intel ne semble pas être correct ici. D'après la citation ci-dessus, REP MOVSW est aussi rapide que REP MOVSD/MOVSQ pour les blocs de mémoire très volumineux, mais des tests ont montré que seuls REP MOVSD/MOVSQ sont rapides, tandis que REP MOVSW est encore 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 compatibles avec le cache N1, il suffit de 4 cycles pour copier 64 octets de données, 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 (aucun ERMSB requis) si les données s’adaptent au cache L1

Voici les tests effectués par REP MOVS * lorsque la source et la destination se trouvaient dans le cache N1, si le nombre de blocs était suffisamment grand pour ne pas être sérieusement affecté par les coûts de démarrage, mais pas assez pour dépasser la taille du cache N1. 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 MOVSB ​​REP amélioré

    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) - avec MOVSB ​​REP amélioré

    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) - avec MOVSB ​​REP amélioré

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

Comme vous le voyez, la mise en œuvre de REP MOVS diffère considérablement d’une microarchitecture à une autre. Sur certains processeurs, comme Ivy Bridge - REP MOVSB ​​est le plus rapide, mais légèrement plus rapide que REP MOVSD/MOVSQ, mais 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 ​​", depuis, sur Ivy Bridge (2013) avec REP MOVSB ​​enchaîné , REP MOVSD affiche les mêmes données octet par heure que sur Nehalem (2010) sans REP MOVSB ​​enchanté , alors que REP MOVSB ​​est devenu très rapide seulement depuis SkyLake (2015) - deux fois plus vite que sur Ivy Bridge. Donc, ce bit REP MOVSB ​​ repéré dans le CPUID peut être déroutant - il montre seulement que REP MOVSB _ en soi est OK, mais pas que n'importe quel REP MOVS* est plus rapide.

L'implémentation d'ERMBSB la plus déroutante concerne la microarchitecture d'Ivy Bridge. Oui, sur les très vieux processeurs, avant ERMSB, REP MOVS * pour les grands blocs utilisait une fonctionnalité de protocole de cache qui n’était pas disponible pour le code standard (no-RFO). Mais ce protocole n'est plus utilisé sur Ivy Bridge qui dispose d'ERMSB. Selon les commentaires d'Andy Glew sur une réponse de "Peter Cordes" à "pourquoi les mémoires compliquées sont-ils compliqués?" , une fonctionnalité de protocole de cache qui n'était pas disponible pour le code standard était autrefois utilisée sur des processeurs plus anciens, mais plus sur Ivy Bridge. Et nous expliquons pourquoi les coûts de démarrage sont si élevés pour REP MOVS *: "Les frais généraux importants pour le choix et la configuration de la bonne méthode sont principalement dus à l’absence de prédiction de branche de microcode". Il est également intéressant de noter qu’en 1996, Pentium Pro (P6) avait implémenté REP MOVS * avec des chargements et des stockages de microcodes 64 bits et un protocole de cache sans RFO.

Avertissement

  1. Cette réponse n'est pertinente que dans les cas où les données source et cible correspondent au cache L1. Selon les circonstances, les particularités de l'accès à la mémoire (cache, etc.) doivent être prises en compte. Prefetch et NTI peuvent donner de meilleurs résultats dans certains cas, en particulier sur les processeurs qui ne disposent pas encore du logiciel Enhanced REP MOVSB. Même sur ces anciens processeurs, REP MOVSD a peut-être utilisé une fonctionnalité de protocole de cache qui n’est pas disponible pour le code standard.
  2. Les informations contenues dans cette réponse concernent uniquement les processeurs Intel et non les processeurs d'autres fabricants tels qu'AMD, susceptibles d'implémenter des instructions REP MOVS * de meilleure ou de pire qualité.
  3. Par souci de confirmation, j’ai présenté les résultats des tests pour SkyLake et Kaby Lake, ces architectures ayant les mêmes données de cycle par instruction.
  4. Tous les noms de produits, marques commerciales et marques déposées appartiennent à leurs propriétaires respectifs.
10
Maxim Masiutin

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 que cela signifie. En regardant la documentation 3.7.6.1 à laquelle vous créez un lien, cela indique explicitement:

la mise en oeuvre de memcpy à l'aide d'ERMSB peut ne pas atteindre le même niveau de débit que l'utilisation d'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 le moyen le plus rapide de copier de la mémoire. Cela signifie simplement qu'il ne sera pas aussi nul que dans certains processeurs précédents.

Cependant, le fait qu’il existe des alternatives qui, dans certaines conditions, permettent d’exécuter plus rapidement ne signifie pas que REP MOVSB ​​est inutile. Maintenant que les pénalités de performance que cette instruction entraînait ont disparu, c'est potentiellement une instruction utile à nouveau.

N'oubliez pas qu'il s'agit d'un petit morceau de code (2 octets!) Comparé à certaines des routines memcpy plus complexes que j'ai vues. Le chargement et l’exécution de gros morceaux de code entraînant également une pénalité (le fait de jeter une partie de votre autre code hors de la mémoire cache du processeur), il arrive que le 'bénéfice' d’AVX et autres soit compensé par son impact sur le reste de votre ordinateur. code. Cela dépend de ce que vous faites.

Vous demandez aussi:

Pourquoi la bande passante est-elle si réduite avec REP MOVSB? Que puis-je faire pour l'améliorer?

Il ne sera pas possible de "faire quelque chose" pour que REP MOVSB ​​s'exécute plus rapidement. Il fait ce qu'il fait.

Si vous souhaitez obtenir les vitesses les plus élevées que vous voyez à partir de memcpy, vous pouvez creuser la source. C'est quelque part quelque part. Ou vous pouvez y faire un suivi à partir d'un débogueur et voir les chemins de code réels empruntés. Je pense 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.

7
David Wohlferd

Ce n'est pas une réponse à la (aux) question (s) énoncée (s), seulement mes résultats (et conclusions personnelles) lorsque je tente de le découvrir.

En résumé: GCC optimise déjà memset()/memmove()/memcpy() (voir par exemple gcc/config/i386/i386.c: expand_set_or_movmem_via_rep () in les sources GCC; recherchez également stringop_algs dans le même fichier pour voir les variantes dépendantes de l'architecture). Il n’ya donc aucune raison de s’attendre à des gains énormes en utilisant votre propre variante avec GCC (à moins que vous n’ayez oublié des attributs importants tels que les attributs d’alignement pour vos données alignées, ou que vous n’ayez pas activé des optimisations suffisamment spécifiques comme -O2 -march= -mtune=). Si vous êtes d’accord, alors les réponses à la question posée sont plus ou moins hors de propos dans la pratique.

(J'aimerais seulement qu'il y ait un memrepeat(), l'opposé de memcpy() par rapport à memmove(), qui répète la partie initiale d'un tampon pour remplir tout le tampon.)


J'ai actuellement une machine Ivy Bridge en cours d'utilisation (ordinateur portable Core i5-6200U, noyau Linux 4.4.0 x86-64, avec les indicateurs erms dans /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 Dépasserait celle d'un simple memcpy(), j'ai écrit un point de repère trop compliqué.

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

Un Xorshift * PRNG est utilisé pour initialiser original avec des données aléatoires. (Comme je l'avais prévenu ci-dessus, c'est trop compliqué, mais je voulais m'assurer Je ne laisse pas de raccourcis faciles pour le compilateur.) La zone correct est obtenue en commençant par 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 vers correct, ceci permet à chaque fonction référencée d'être vérifiée et qu'elle 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 celles-ci est utilisée pour la comparaison. (À mon avis, la médiane est ce qui convient le mieux dans l'analyse comparative et fournit une sémantique judicieuse - la fonction est au moins aussi rapide, au moins la moitié du temps.)

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

Malheureusement, je n'ai pas encore trouvé de jeu 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" );
}

battrait

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 en utilisant GCC 5.4.0 sur l’ordinateur portable Core i5-6200U susmentionné exécutant un noyau linux-4.4.0 64 bits. La copie de morceaux alignés et dimensionnés sur 4 096 octets est cependant proche.

Cela signifie qu'au moins jusqu'à présent, je n'ai pas trouvé de cas où l'utilisation d'une variante de rep movsb Memcpy aurait un sens. Cela ne veut pas dire qu'il n'y a pas un tel cas; Je n'en ai tout simplement pas trouvé.

(À ce stade, le code est un gâchis de spaghettis dont j'ai plus honte que fier, alors je vais omettre de publier les sources à moins que quelqu'un ne le demande. La description ci-dessus devrait suffire à en écrire un meilleur.)


Cela ne me surprend pas beaucoup, cependant. Le compilateur C peut déduire de nombreuses informations sur l'alignement des pointeurs d'opérandes et sur le fait de savoir si le nombre d'octets à copier est une constante de compilation, un multiple d'une puissance appropriée de deux. Le compilateur peut utiliser et doit/devrait utiliser ces informations pour remplacer les fonctions de la bibliothèque C memcpy()/memmove().

GCC fait exactement cela (voir, par exemple, gcc/config/i386/i386.c: expand_set_or_movmem_via_rep () dans les sources de GCC; recherchez également stringop_algs Dans le même fichier pour déterminer si l'architecture est dépendante. variantes). En effet, memcpy()/memset()/memmove() a déjà été optimisé séparément pour de nombreuses variantes de processeurs x86; Cela m'étonnerait si les développeurs de GCC n'avaient pas déjà inclus le support d'erms.

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

L’un des motifs les plus courants que j’utilise dans mon code est

some_type *pointer = __builtin_assume_aligned(ptr, alignment);

ptr est un pointeur, alignment est le nombre d'octets sur lesquels il est aligné; GCC sait alors/suppose que pointer est aligné sur alignment octets.

Un autre élément utile, bien que beaucoup plus difficile à utiliser correctement , est __builtin_prefetch() . Pour maximiser l'efficacité globale de la bande passante, j'ai constaté que la réduction des latences dans chaque sous-opération produisait les meilleurs résultats. (Il est difficile de copier des éléments dispersés vers un stockage temporaire consécutif, car la prélecture implique généralement une ligne de cache complète; si trop d'éléments sont préludés, la majeure partie du cache est gaspillée par le stockage d'éléments inutilisés.)

6
Nominal Animal

Il existe des moyens beaucoup plus efficaces pour déplacer les données. Ces jours-ci, la mise en œuvre de memcpy générera un code spécifique à l’architecture à partir du compilateur, optimisé en fonction de l’alignement en 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 du monde x86.

Quand vous codez en dur rep movsb empêche cette utilisation d’intrinsèques.

Par conséquent, pour quelque chose comme un memcpy, sauf si vous écrivez quelque chose qui sera lié à un matériel très spécifique et à moins que vous preniez le temps d’écrire une fonction hautement optimisée memcpy dans Assembly (ou en utilisant des intrinsèques de niveau C), vous êtes loin mieux à même de permettre au compilateur de le comprendre pour vous.

3
David Hoelzer

En règle générale memcpy() guide:

a) Si les données en cours de copie sont minuscules (moins de 20 octets) et ont une taille fixe, laissez le compilateur le faire. Raison: le compilateur peut utiliser les instructions normales mov et éviter les frais généraux de démarrage.

b) Si les données en cours de copie sont réduites (moins d’environ 4 Ko) et que l’alignement est garanti, utilisez rep movsb (si ERMSB est pris en charge) ou rep movsd (si ERMSB n'est pas pris en charge). Raison: L'utilisation d'une alternative SSE ou AVX génère une "surcharge de démarrage" énorme avant de copier quoi que ce soit.

c) Si les données copiées sont petites (moins de 4 Ko environ) et que l'alignement n'est pas garanti, utilisez rep movsb. Raison: Utiliser SSE ou AVX, ou utiliser rep movsd pour l'essentiel plus quelques rep movsb au début ou à la fin, a trop de frais généraux.

d) Dans 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 si lent que cela forcera les programmeurs à trouver une alternative qui n'implique pas la copie d'énormes globes de données; et le logiciel résultant sera beaucoup plus rapide, car la copie de grandes quantités de données a été évitée.

1
Brendan