web-dev-qa-db-fra.com

Pourquoi memcpy () et memmove () sont plus rapides que les incréments de pointeur?

Je copie N octets de pSrc vers pDest. Cela peut être fait en une seule boucle:

for (int i = 0; i < N; i++)
    *pDest++ = *pSrc++

Pourquoi est-ce plus lent que memcpy ou memmove? Quelles astuces utilisent-ils pour l'accélérer?

90
wanderer

Parce que memcpy utilise des pointeurs Word au lieu de pointeurs d'octets, les implémentations memcpy sont également souvent écrites avec des instructions SIMD qui permettent de mélanger 128 bits à la fois.

Les instructions SIMD sont des instructions d'assemblage qui peuvent effectuer la même opération sur chaque élément d'un vecteur jusqu'à 16 octets de long. Cela comprend les instructions de chargement et de stockage.

115
onemasse

Les routines de copie de mémoire peuvent être beaucoup plus compliquées et plus rapides qu'une simple copie de mémoire via des pointeurs tels que:

void simple_memory_copy(void* dst, void* src, unsigned int bytes)
{
  unsigned char* b_dst = (unsigned char*)dst;
  unsigned char* b_src = (unsigned char*)src;
  for (int i = 0; i < bytes; ++i)
    *b_dst++ = *b_src++;
}

Améliorations

La première amélioration que l'on peut apporter consiste à aligner l'un des pointeurs sur une frontière de Word (par Word, je veux dire la taille entière native, généralement 32 bits/4 octets, mais peut être 64 bits/8 octets sur les architectures plus récentes) et utiliser le déplacement de taille Word/copier les instructions. Cela nécessite l'utilisation d'une copie octet à octet jusqu'à ce qu'un pointeur soit aligné.

void aligned_memory_copy(void* dst, void* src, unsigned int bytes)
{
  unsigned char* b_dst = (unsigned char*)dst;
  unsigned char* b_src = (unsigned char*)src;

  // Copy bytes to align source pointer
  while ((b_src & 0x3) != 0)
  {
    *b_dst++ = *b_src++;
    bytes--;
  }

  unsigned int* w_dst = (unsigned int*)b_dst;
  unsigned int* w_src = (unsigned int*)b_src;
  while (bytes >= 4)
  {
    *w_dst++ = *w_src++;
    bytes -= 4;
  }

  // Copy trailing bytes
  if (bytes > 0)
  {
    b_dst = (unsigned char*)w_dst;
    b_src = (unsigned char*)w_src;
    while (bytes > 0)
    {
      *b_dst++ = *b_src++;
      bytes--;
    }
  }
}

Différentes architectures fonctionneront différemment selon que la source ou le pointeur de destination est correctement aligné. Par exemple, sur un processeur XScale, j'ai obtenu de meilleures performances en alignant le pointeur de destination plutôt que le pointeur source.

Pour améliorer encore les performances, un certain déroulement de boucle peut être effectué, de sorte que davantage de registres du processeur sont chargés de données et cela signifie que les instructions de chargement/stockage peuvent être entrelacées et que leur latence est masquée par des instructions supplémentaires (telles que le comptage de boucles, etc.). L'avantage que cela apporte varie considérablement selon le processeur, car les latences des instructions de chargement/stockage peuvent être très différentes.

À ce stade, le code finit par être écrit dans Assembly plutôt qu'en C (ou C++) car vous devez placer manuellement les instructions de chargement et de stockage pour tirer le meilleur parti du masquage de latence et du débit.

En général, toute une ligne de données de cache doit être copiée en une seule itération de la boucle non déroulée.

Ce qui m'amène à la prochaine amélioration, l'ajout de la prélecture. Ce sont des instructions spéciales qui indiquent au système de cache du processeur de charger des parties spécifiques de la mémoire dans son cache. Comme il y a un délai entre l'émission de l'instruction et le remplissage de la ligne de cache, les instructions doivent être placées de manière à ce que les données soient disponibles au moment où elles doivent être copiées, et pas plus tôt/plus tard.

Cela signifie mettre des instructions de prélecture au début de la fonction ainsi qu'à l'intérieur de la boucle de copie principale. Avec les instructions de prélecture au milieu de la boucle de copie, récupérer les données qui seront copiées en plusieurs itérations.

Je ne me souviens pas, mais il peut également être avantageux de pré-extraire les adresses de destination ainsi que les sources.

Facteurs

Les principaux facteurs qui affectent la vitesse de copie de la mémoire sont les suivants:

  • La latence entre le processeur, ses caches et la mémoire principale.
  • La taille et la structure des lignes de cache du processeur.
  • Les instructions de déplacement/copie de la mémoire du processeur (latence, débit, taille de registre, etc.).

Donc, si vous voulez écrire une routine de gestion de la mémoire efficace et rapide, vous devrez en savoir beaucoup sur le processeur et l'architecture pour lesquels vous écrivez. Il suffit de dire qu'à moins que vous n'écriviez sur une plate-forme intégrée, il serait beaucoup plus facile d'utiliser simplement les routines de copie de mémoire intégrées.

79
Daemin

memcpy peut copier plus d'un octet à la fois selon l'architecture de l'ordinateur. La plupart des ordinateurs modernes peuvent fonctionner avec 32 bits ou plus dans une seule instruction de processeur.

De n exemple d'implémentation :

 00026 * Pour une copie rapide, optimisez le cas courant où les deux pointeurs 
 00027 * et la longueur sont alignés sur Word, et copiez Word-at-a-time à la place 
 00028 * d'octet à la fois. Sinon, copiez par octets. 
18
Mark Byers

Vous pouvez implémenter memcpy() en utilisant l'une des techniques suivantes, certaines dépendant de votre architecture pour des gains de performances, et elles seront toutes beaucoup plus rapides que votre code:

  1. Utilisez des unités plus grandes, telles que des mots de 32 bits au lieu d'octets. Vous pouvez également (ou devoir) gérer l'alignement ici également. Vous ne pouvez pas aller lire/écrire un mot 32 bits dans un emplacement de mémoire étrange, par exemple sur certaines plates-formes, et sur d'autres plates-formes, vous payez une énorme pénalité de performances. Pour résoudre ce problème, l'adresse doit être une unité divisible par 4. Vous pouvez prendre cela jusqu'à 64 bits pour les processeurs 64 bits, ou même plus en utilisant SIMD (Instruction unique, données multiples) instructions ( MMX , SSE , etc.)

  2. Vous pouvez utiliser des instructions CPU spéciales que votre compilateur peut ne pas être en mesure d'optimiser à partir de C. Par exemple, sur un 80386, vous pouvez utiliser l'instruction de préfixe "rep" + l'instruction "movsb" pour déplacer N octets dictés en plaçant N dans le nombre registre. Les bons compilateurs le feront juste pour vous, mais vous pouvez être sur une plate-forme qui manque d'un bon compilateur. Notez que cet exemple a tendance à être une mauvaise démonstration de la vitesse, mais combiné avec l'alignement + des instructions d'unité plus grandes, il peut être plus rapide que la plupart des autres sur certains CPU.

  3. Déroulement des boucles - les branches peuvent être assez chères sur certains CPU, donc le déroulage des boucles peut réduire le nombre de branches. C'est aussi une bonne technique pour combiner avec des instructions SIMD et de très grandes unités.

Par exemple, http://www.agner.org/optimize/#asmlib a une implémentation memcpy qui bat le plus (là-bas). Si vous lisez le code source, il sera plein de tonnes de code d'assemblage intégré qui tirera parti des trois techniques ci-dessus, en choisissant laquelle de ces techniques en fonction du processeur sur lequel vous exécutez.

Remarque: il existe également des optimisations similaires pour trouver des octets dans un tampon. strchr() et vos amis seront souvent plus rapides que votre équivalent roulé à la main. Cela est particulièrement vrai pour . NET et Java . Par exemple, dans .NET, la String.IndexOf() intégrée est beaucoup plus rapide que même une recherche de chaîne Boyer – Moore , car elle utilise les techniques d'optimisation ci-dessus.

7
Danny Dulai

Réponse courte:

  • remplissage du cache
  • transferts de mots au lieu de ceux d'octets lorsque cela est possible
  • Magie SIMD
5
moshbear

Je ne sais pas s'il est réellement utilisé dans les implémentations réelles de memcpy, mais je pense que Duff's Device mérite une mention ici.

De Wikipedia :

send(to, from, count)
register short *to, *from;
register count;
{
        register n = (count + 7) / 8;
        switch(count % 8) {
        case 0:      do {     *to = *from++;
        case 7:              *to = *from++;
        case 6:              *to = *from++;
        case 5:              *to = *from++;
        case 4:              *to = *from++;
        case 3:              *to = *from++;
        case 2:              *to = *from++;
        case 1:              *to = *from++;
                } while(--n > 0);
        }
}

Notez que ce qui précède n'est pas un memcpy car il n'incrémente pas délibérément le pointeur to. Il implémente une opération légèrement différente: l'écriture dans un registre mappé en mémoire. Voir l'article Wikipedia pour plus de détails.

4
NPE

Comme d'autres disent des copies memcpy plus grandes que des morceaux de 1 octet. La copie en morceaux de taille Word est beaucoup plus rapide. Cependant, la plupart des implémentations vont plus loin et exécutent plusieurs instructions MOV (Word) avant de boucler. L'avantage de copier, disons, 8 blocs de mots par boucle est que la boucle elle-même est coûteuse. Cette technique réduit le nombre de branches conditionnelles d'un facteur 8, optimisant la copie pour les blocs géants.

3
VoidStar

Les réponses sont excellentes, mais si vous voulez toujours implémenter vous-même un memcpy rapide, il y a un article de blog intéressant sur la memcpy rapide, Memcpy rapide en C = .

void *memcpy(void* dest, const void* src, size_t count)
{
    char* dst8 = (char*)dest;
    char* src8 = (char*)src;

    if (count & 1) {
        dst8[0] = src8[0];
        dst8 += 1;
        src8 += 1;
    }

    count /= 2;
    while (count--) {
        dst8[0] = src8[0];
        dst8[1] = src8[1];

        dst8 += 2;
        src8 += 2;
    }
    return dest;
}

Même, il peut être préférable d'optimiser les accès à la mémoire.

2
deepmax

Parce que, comme de nombreuses routines de bibliothèque, il a été optimisé pour l'architecture sur laquelle vous exécutez. D'autres ont publié diverses techniques qui peuvent être utilisées.

Si vous avez le choix, utilisez des routines de bibliothèque plutôt que de lancer les vôtres. Il s'agit d'une variation de DRY que j'appelle DRO (Don't Repeat Others). De plus, les routines de bibliothèque sont moins susceptibles d'être erronées que votre propre implémentation.

J'ai vu des vérificateurs d'accès à la mémoire se plaindre de lectures hors limites sur la mémoire ou de tampons de chaîne qui n'étaient pas un multiple de la taille de Word. Ceci est le résultat de l'optimisation utilisée.

1
BillThor