web-dev-qa-db-fra.com

Pourquoi memmove est-il plus rapide que memcpy?

J'étudie les points chauds de performance dans une application qui consacre 50% de son temps à memmove (3). L'application insère des millions d'entiers de 4 octets dans des tableaux triés et utilise memmove pour décaler les données "à droite" afin de libérer de l'espace pour la valeur insérée.

Je m'attendais à ce que la copie de mémoire soit extrêmement rapide, et j'ai été surpris de constater le temps passé à mémoriser. Mais ensuite, j'ai eu l'idée que memmove est lent car il déplace des régions qui se chevauchent, ce qui doit être implémenté dans une boucle étroite au lieu de copier de grandes pages de mémoire. J'ai écrit un petit micro-repère pour savoir s'il y avait une différence de performance entre memcpy et memmove, en espérant que ce dernier gagne haut la main.

J'ai exécuté ma référence sur deux machines (Core i5, Core i7) et j'ai constaté que memmove est en réalité plus rapide que memcpy, sur l'ancien Core i7 presque deux fois plus vite! Maintenant je cherche des explications.

Voici mon point de repère. Il copie 100 mb avec memcpy, puis se déplace d’environ 100 mb avec memmove; la source et la destination se chevauchent. Différentes "distances" pour la source et la destination sont essayées. Chaque test est exécuté 10 fois, le temps moyen est imprimé.

https://Gist.github.com/cruppstahl/78a57cdf937bca3d062c

Voici les résultats sur le Core i5 (Linux 3.5.0-54-generic # 81 ~ précis1-Ubuntu SMP x86_64 GNU/Linux, gcc est 4.6.3 (Ubuntu/Linaro 4.6.3-1ubuntu5). Le nombre entre parenthèses est la distance (taille de l'espace) entre la source et la destination:

memcpy        0.0140074
memmove (002) 0.0106168
memmove (004) 0.01065
memmove (008) 0.0107917
memmove (016) 0.0107319
memmove (032) 0.0106724
memmove (064) 0.0106821
memmove (128) 0.0110633

Memmove est implémenté en tant que code assembleur optimisé SSE). Il utilise la lecture anticipée par le matériel pour charger les données dans le cache. Il copie 128 octets dans des registres XMM, puis les stocke à la destination.

( memcpy-ssse3-back.S , lignes 1650 et suivantes)

L(gobble_ll_loop):
    prefetchnta -0x1c0(%rsi)
    prefetchnta -0x280(%rsi)
    prefetchnta -0x1c0(%rdi)
    prefetchnta -0x280(%rdi)
    sub $0x80, %rdx
    movdqu  -0x10(%rsi), %xmm1
    movdqu  -0x20(%rsi), %xmm2
    movdqu  -0x30(%rsi), %xmm3
    movdqu  -0x40(%rsi), %xmm4
    movdqu  -0x50(%rsi), %xmm5
    movdqu  -0x60(%rsi), %xmm6
    movdqu  -0x70(%rsi), %xmm7
    movdqu  -0x80(%rsi), %xmm8
    movdqa  %xmm1, -0x10(%rdi)
    movdqa  %xmm2, -0x20(%rdi)
    movdqa  %xmm3, -0x30(%rdi)
    movdqa  %xmm4, -0x40(%rdi)
    movdqa  %xmm5, -0x50(%rdi)
    movdqa  %xmm6, -0x60(%rdi)
    movdqa  %xmm7, -0x70(%rdi)
    movdqa  %xmm8, -0x80(%rdi)
    lea -0x80(%rsi), %rsi
    lea -0x80(%rdi), %rdi
    jae L(gobble_ll_loop)

Pourquoi memmove est-il plus rapide que memcpy? Je m'attendrais à ce que memcpy copie les pages mémoire, ce qui devrait être beaucoup plus rapide que la boucle. Dans le pire des cas, je m'attendrais à ce que memcpy soit aussi rapide que memmove.

PS: Je sais que je ne peux pas remplacer memmove par memcpy dans mon code. Je sais que l'exemple de code mélange C et C++. Cette question est vraiment juste pour des buts académiques.

MISE À JOUR 1

J'ai couru quelques variantes des tests, en fonction des différentes réponses.

  1. Lorsque vous exécutez memcpy deux fois, la deuxième exécution est plus rapide que la première.
  2. Lorsque vous "touchez" la mémoire tampon de destination de memcpy (memset(b2, 0, BUFFERSIZE...)), la première exécution de memcpy est également plus rapide.
  3. memcpy est encore un peu plus lent que memmove.

Voici les résultats:

memcpy        0.0118526
memcpy        0.0119105
memmove (002) 0.0108151
memmove (004) 0.0107122
memmove (008) 0.0107262
memmove (016) 0.0108555
memmove (032) 0.0107171
memmove (064) 0.0106437
memmove (128) 0.0106648

Ma conclusion: sur la base d'un commentaire de @Oliver Charlesworth, le système d'exploitation doit valider la mémoire physique dès que le tampon de destination memcpy est utilisé pour la première fois (si quelqu'un sait comment le "valider", ajoutez une réponse! ). En outre, comme l'a dit @Mats Petersson, memmove est plus convivial en cache que memcpy.

Merci pour toutes les bonnes réponses et commentaires!

87
cruppstahl

Vos appels memmove mélangent de 2 à 128 octets de mémoire, alors que vos source et destination memcpy sont complètement différentes. En quelque sorte, cela explique la différence de performance: si vous copiez au même endroit, vous verrez que memcpy finit peut-être par une petite perte de temps, par exemple. sur ideone.com :

memmove (002) 0.0610362
memmove (004) 0.0554264
memmove (008) 0.0575859
memmove (016) 0.057326
memmove (032) 0.0583542
memmove (064) 0.0561934
memmove (128) 0.0549391
memcpy 0.0537919

Quasiment rien dedans cependant - aucune preuve que réécrire sur une page déjà fausse en mémoire a beaucoup d'impact , et nous ne voyons certainement pas une réduction de moitié de le temps ... mais cela montre qu'il n'y a rien de mal à rendre memcpy inutilement plus lent lorsque l'on compare des pommes pour des pommes.

55
Tony Delroy

Lorsque vous utilisez memcpy, les écritures doivent être placées dans le cache. Lorsque vous utilisez memmove où, lorsque vous copiez un petit pas en avant, la mémoire que vous copiez est déjà dans le cache (car elle a été lue à 2, 4, 16 ou 128 octets "en arrière"). Essayez de faire un memmove où la destination est de plusieurs mégaoctets (> 4 * taille du cache), et je suppose (mais vous ne pouvez pas vous soucier de tester) que vous obtiendrez des résultats similaires.

Je vous garantis que ALL concerne la maintenance du cache lorsque vous effectuez des opérations de mémoire volumineuse.

22
Mats Petersson

Historiquement, memmove et memcopy ont la même fonction. Ils ont travaillé de la même manière et ont eu la même implémentation. On s'est alors rendu compte que memcopy n'avait pas besoin (et n'était souvent pas) défini pour gérer les zones de chevauchement d'une manière particulière.

Le résultat final est que memmove a été défini pour gérer les régions qui se chevauchent de manière particulière, même si cela a un impact sur les performances. Memcopy est censé utiliser le meilleur algorithme disponible pour les régions ne se chevauchant pas. Les implémentations sont normalement presque identiques.

Le problème que vous avez rencontré est qu’il existe tellement de variantes du matériel x86 qu’il est impossible de dire quelle méthode de décalage de la mémoire sera la plus rapide. Et même si vous pensez que vous avez un résultat dans une circonstance, quelque chose d'aussi simple que d'avoir une "foulée" différente dans la disposition de la mémoire peut entraîner des performances de cache considérablement différentes.

Vous pouvez soit comparer ce que vous faites réellement, soit ignorer le problème et vous appuyer sur les tests effectués pour la bibliothèque C.

Edit: Oh, et une dernière chose; déplacer beaucoup de contenu de la mémoire est TRÈS lent. Je suppose que votre application fonctionnerait plus rapidement avec quelque chose comme une implémentation simple de B-Tree pour gérer vos entiers. (Oh tu l'es, d'accord)

Edit2: Pour résumer mon développement dans les commentaires: Le microbien référent est le problème ici, il ne mesure pas ce que vous pensez. Les tâches confiées à memcpy et memmove sont très différentes l'une de l'autre. Si la tâche confiée à memcpy est répétée plusieurs fois avec memmove ou memcpy, les résultats finaux ne dépendront pas de la fonction de décalage de mémoire que vous utilisez, SAUF SI les régions se chevauchent.

15
user3710044

"memcpy est plus efficace que memmove." Dans votre cas, vous ne faites probablement pas exactement la même chose lorsque vous exécutez les deux fonctions.

En règle générale, USE memmove uniquement si vous devez le faire. UTILISEZ-le quand il y a une chance très raisonnable que les régions source et cible se chevauchent.

Référence: https://www.youtube.com/watch?v=Yr1YnOVG-4g Dr. Jerry Cain, (Conférence de Stanford Intro Systems - 7) Heure: 36:00

2
Ehsan