différences de performance memcpy entre les processus 32 et 64 bits

Nous avons des machines Core2 (Dell T5400) avec XP64.

Nous observons que lors de l’exécution de processus 32 bits, les performances de memcpy sont de l’ordre de 1,2 Go / s; Cependant, memcpy dans un processus 64 bits atteint environ 2,2 Go / s (ou 2,4 Go / s avec le memcpy du compilateur Intel). Alors que la réaction initiale pourrait être d’expliquer cela simplement en raison des registres plus larges disponibles en code 64 bits, nous observons que notre propre code d’assemblage SSE de type memcpy (qui devrait utiliser des magasins de chargement de largeur 128 bits indépendamment de 32 / 64-bitness du processus) montre des limites supérieures similaires sur la bande passante de copie qu’il atteint.

Ma question est la suivante: quelle est la différence réelle? Est-ce que les processus 32 bits doivent passer par des cerceaux WOW64 supplémentaires pour accéder à la RAM? Est-ce quelque chose à voir avec les TLB ou les pré-récupérateurs ou … quoi?

Merci pour toute idée.

Également soulevé sur les forums Intel .

    Bien sûr, vous devez vraiment regarder les instructions de la machine en cours d’exécution dans la boucle la plus interne du memcpy, en entrant dans le code machine avec un débogueur. Tout le rest n’est que de la spéculation.

    Ma question est que cela n’a probablement rien à voir avec 32 bits contre 64 bits en soi; Je suppose que la routine de bibliothèque plus rapide a été écrite en utilisant des magasins SSE non temporels.

    Si la boucle interne contient une variation des instructions de stockage de chargement classiques, la mémoire de destination doit être lue dans le cache de la machine, modifiée et réécrite. Étant donné que cette lecture est totalement inutile – les bits en cours de lecture sont écrasés immédiatement – vous pouvez économiser la moitié de la bande passante en utilisant les instructions d’écriture “non temporelles”, qui contournent les caches. De cette façon, la mémoire de destination est simplement écrite en effectuant un aller simple dans la mémoire au lieu d’un aller-retour.

    Je ne connais pas la bibliothèque CRT du compilateur Intel, alors ce n’est qu’une supposition. Il n’y a pas de raison particulière pour que la libCRT 32 bits ne puisse pas faire la même chose, mais l’accélération que vous citez est à la limite de ce que j’attendais en convertissant les instructions movdqa en movnt …

    Comme memcpy ne fait aucun calcul, il est toujours lié à la vitesse à laquelle vous pouvez lire et écrire de la mémoire.

    Je pense que ce qui suit peut l’expliquer:

    Pour copier des données de la mémoire dans un registre et de nouveau en mémoire,

    mov eax, [address] mov [address2], eax 

    Cela déplace 32 bits (4 octets) de l’adresse à l’adresse2. La même chose se passe avec 64 bits en mode 64 bits

     mov rax, [address] mov [address2], rax 

    Cela déplace 64 bits, 2 octets, de l’adresse à l’adresse2. “mov” lui-même, que ce soit 64 bits ou 32 bits a une latence de 0,5 et un débit de 0,5 selon les spécifications d’Intel. La latence est le nombre de cycles d’horloge que l’instruction prend pour parcourir le pipeline et le débit est la durée pendant laquelle le processeur doit attendre avant d’accepter à nouveau la même instruction. Comme vous pouvez le voir, il peut faire deux mov par cycle d’horloge, cependant, il doit attendre un demi-cycle d’horloge entre deux mov, ainsi il ne peut effectivement faire qu’un mov par cycle d’horloge (ou suis-je mal ici et mal interpréter les termes?) Voir PDF ici pour plus de détails).

    Bien sûr mov reg, mem peut être plus long que 0,5 cycle, selon que les données sont dans le cache de premier ou deuxième niveau, ou pas du tout dans le cache et doit être récupéré de la mémoire. Cependant, le temps de latence ci-dessus ignore ce fait (comme l’indique le PDF ci-dessus), toutes les données nécessaires au mov sont déjà présentes (sinon, la latence augmentera du temps nécessaire à la récupération des données). Pour le moment, cela peut être plusieurs cycles d’horloge et est complètement indépendant de la commande en cours d’exécution, dit le PDF à la page 482 / C-30).

    Ce qui est intéressant, que le mov soit 32 ou 64 bits ne joue aucun rôle. Cela signifie qu’à moins que la bande passante mémoire ne devienne un facteur limitant, les mov 64 bits sont également rapides aux mov 32 bits, et comme il ne faut que deux fois moins de mov pour déplacer la même quantité de données de A à B (en théorie) être deux fois plus élevé (le fait que ce ne soit pas le cas probablement parce que la mémoire n’est pas illimitée rapidement).

    Ok, maintenant vous pensez que lorsque vous utilisez les registres SSE plus grands, vous devriez obtenir un débit plus rapide, non? AFAIK les registres xmm ne sont pas 256, mais 128 bits de large, BTW ( référence sur Wikipedia ). Cependant, avez-vous envisagé la latence et le débit? Les données que vous souhaitez déplacer sont alignées sur 128 bits ou non. Selon cela, vous le déplacez soit en utilisant

     movdqa xmm1, [address] movdqa [address2], xmm1 

    ou si pas aligné

     movdqu xmm1, [address] movdqu [address2], xmm1 

    Eh bien, movdqa / movdqu a une latence de 1 et un débit de 1. Donc, les instructions prennent deux fois plus de temps pour être exécutées et le temps d’attente après les instructions est deux fois plus long qu’un mov normal.

    Et quelque chose que nous n’avons même pas pris en compte est le fait que le CPU divise réellement les instructions en micro-opérations et qu’il peut les exécuter en parallèle. Maintenant ça commence à devenir vraiment compliqué … même trop compliqué pour moi.

    Quoi qu’il en soit, je sais par expérience que le chargement de données vers / depuis des registres xmm est beaucoup plus lent que le chargement de données depuis / vers des registres normaux. Je suis en fait surpris que le SSE memmove ne soit pas beaucoup plus lent que le normal.

    Je suis finalement arrivé au fond des choses (et la réponse de Die in Sente était sur la bonne voie, merci)

    Dans ce qui suit, dst et src sont 512 MByte std :: vector. J’utilise le compilateur Intel 10.1.029 et CRT.

    Sur 64 bits les deux

    memcpy(&dst[0],&src[0],dst.size())

    et

    memcpy(&dst[0],&src[0],N)

    où N est précédemment déclaré const size_t N=512*(1<<20); appel

    __intel_fast_memcpy

    dont l'essentiel est constitué de:

      000000014004ED80 lea rcx,[rcx+40h] 000000014004ED84 lea rdx,[rdx+40h] 000000014004ED88 lea r8,[r8-40h] 000000014004ED8C prefetchnta [rdx+180h] 000000014004ED93 movdqu xmm0,xmmword ptr [rdx-40h] 000000014004ED98 movdqu xmm1,xmmword ptr [rdx-30h] 000000014004ED9D cmp r8,40h 000000014004EDA1 movntdq xmmword ptr [rcx-40h],xmm0 000000014004EDA6 movntdq xmmword ptr [rcx-30h],xmm1 000000014004EDAB movdqu xmm2,xmmword ptr [rdx-20h] 000000014004EDB0 movdqu xmm3,xmmword ptr [rdx-10h] 000000014004EDB5 movntdq xmmword ptr [rcx-20h],xmm2 000000014004EDBA movntdq xmmword ptr [rcx-10h],xmm3 000000014004EDBF jge 000000014004ED80 

    et fonctionne à ~ 2200 MByte / s.

    Mais sur 32 bits

    memcpy(&dst[0],&src[0],dst.size())

    appels

    __intel_fast_memcpy

    dont la majeure partie consiste en

      004447A0 sub ecx,80h 004447A6 movdqa xmm0,xmmword ptr [esi] 004447AA movdqa xmm1,xmmword ptr [esi+10h] 004447AF movdqa xmmword ptr [edx],xmm0 004447B3 movdqa xmmword ptr [edx+10h],xmm1 004447B8 movdqa xmm2,xmmword ptr [esi+20h] 004447BD movdqa xmm3,xmmword ptr [esi+30h] 004447C2 movdqa xmmword ptr [edx+20h],xmm2 004447C7 movdqa xmmword ptr [edx+30h],xmm3 004447CC movdqa xmm4,xmmword ptr [esi+40h] 004447D1 movdqa xmm5,xmmword ptr [esi+50h] 004447D6 movdqa xmmword ptr [edx+40h],xmm4 004447DB movdqa xmmword ptr [edx+50h],xmm5 004447E0 movdqa xmm6,xmmword ptr [esi+60h] 004447E5 movdqa xmm7,xmmword ptr [esi+70h] 004447EA add esi,80h 004447F0 movdqa xmmword ptr [edx+60h],xmm6 004447F5 movdqa xmmword ptr [edx+70h],xmm7 004447FA add edx,80h 00444800 cmp ecx,80h 00444806 jge 004447A0 

    et fonctionne à ~ 1350 Mo / s seulement.

    TOUTEFOIS

     memcpy(&dst[0],&src[0],N) 

    où N est précédemment déclaré const size_t N=512*(1<<20); comstack (sur 32 bits) un appel direct à un

     __intel_VEC_memcpy 

    dont la majeure partie consiste en

      0043FF40 movdqa xmm0,xmmword ptr [esi] 0043FF44 movdqa xmm1,xmmword ptr [esi+10h] 0043FF49 movdqa xmm2,xmmword ptr [esi+20h] 0043FF4E movdqa xmm3,xmmword ptr [esi+30h] 0043FF53 movntdq xmmword ptr [edi],xmm0 0043FF57 movntdq xmmword ptr [edi+10h],xmm1 0043FF5C movntdq xmmword ptr [edi+20h],xmm2 0043FF61 movntdq xmmword ptr [edi+30h],xmm3 0043FF66 movdqa xmm4,xmmword ptr [esi+40h] 0043FF6B movdqa xmm5,xmmword ptr [esi+50h] 0043FF70 movdqa xmm6,xmmword ptr [esi+60h] 0043FF75 movdqa xmm7,xmmword ptr [esi+70h] 0043FF7A movntdq xmmword ptr [edi+40h],xmm4 0043FF7F movntdq xmmword ptr [edi+50h],xmm5 0043FF84 movntdq xmmword ptr [edi+60h],xmm6 0043FF89 movntdq xmmword ptr [edi+70h],xmm7 0043FF8E lea esi,[esi+80h] 0043FF94 lea edi,[edi+80h] 0043FF9A dec ecx 0043FF9B jne ___intel_VEC_memcpy+244h (43FF40h) 

    et fonctionne à ~ 2100Mo / s (et prouver que 32bits n'est pas une bande passante limitée).

    Je retire mon affirmation selon laquelle mon propre code SSE de type memcpy souffre d'une limite de ~ 1300 Mo / s en version 32 bits; Je n'ai maintenant aucun problème pour obtenir> 2 Go / s sur 32 ou 64 bits; L'astuce (comme l' _mm_stream_ps le résultat ci-dessus) consiste à utiliser des magasins non temporels ("streaming") (par exemple, _mm_stream_ps insortingnsèque).

    Il semble un peu étrange que le " dst.size() " 32 bits de dst.size() n'appelle pas la version " movnt " la plus rapide (si vous entrez dans memcpy, il y a une quantité incroyable de logique CPUID et de logique heuristique. pour être copié avec la taille du cache, etc. avant qu'il ne soit proche de vos données réelles, mais au moins, je comprends le comportement observé maintenant (et ce n'est pas lié à SysWow64 ou H / W).

    Je suppose que les processus 64 bits utilisent la taille de mémoire 64 bits native du processeur, ce qui optimise l’utilisation du bus de mémoire.

    Merci pour les commentaires positifs! Je pense que je peux en partie expliquer ce qui se passe ici.

    Utiliser les magasins non-temporels pour memcpy est certainement le jeûne si vous ne programmez que l’appel memcpy.

    Par contre, si vous comparez une application, les magasins movdqa ont l’avantage de laisser la mémoire de destination en cache. Ou du moins la partie qui rentre dans le cache.

    Donc, si vous concevez une bibliothèque d’exécution et que vous pouvez supposer que l’application qui a appelé memcpy va utiliser le tampon de destination immédiatement après l’appel memcpy, vous voudrez alors fournir la version movdqa. Cela optimise efficacement le trajet entre la mémoire et le processeur qui suivrait la version movntdq, et toutes les instructions qui suivront l’appel seront plus rapides.

    Mais d’un autre côté, si le tampon de destination est volumineux par rapport au cache du processeur, cette optimisation ne fonctionne pas et la version movntdq vous donnera des tests plus rapides.

    Donc, l’idée memcpy aurait plusieurs versions sous le capot. Lorsque le tampon de destination est petit comparé au cache du processeur, utilisez movdqa, sinon, le tampon de destination est volumineux par rapport au cache du processeur, utilisez movntdq. Cela ressemble à ce qui se passe dans la bibliothèque 32 bits.

    Bien sûr, rien de tout cela n’a à voir avec les différences entre 32 bits et 64 bits.

    Ma conjecture est que la bibliothèque 64 bits n’est pas aussi mature. Les développeurs ne sont pas encore parvenus à fournir les deux routines dans cette version de la bibliothèque.

    Je n’ai pas de référence en face de moi, donc je ne suis pas absolument sûr des timings / instructions, mais je peux quand même donner la théorie. Si vous faites un mouvement de mémoire en mode 32 bits, vous ferez quelque chose comme un “rep movsd” qui déplace une seule valeur de 32 bits à chaque cycle d’horloge. En mode 64 bits, vous pouvez faire un “rep movsq” qui effectue un seul mouvement de 64 bits à chaque cycle d’horloge. Cette instruction n’est pas disponible pour le code 32 bits, vous feriez donc 2 x rep movsd (à 1 cycle par morceau) pour la moitié de la vitesse d’exécution.

    TRÈS simplifié, en ignorant tous les problèmes de bande passante mémoire / alignement, etc., mais c’est là que tout commence …

    Voici un exemple de routine memcpy spécifiquement conçue pour l’architecture 64 bits.

     void uint8copy(void *dest, void *src, size_t n){ uint64_t * ss = (uint64_t)src; uint64_t * dd = (uint64_t)dest; n = n * sizeof(uint8_t)/sizeof(uint64_t); while(n--) *dd++ = *ss++; }//end uint8copy() 

    L’article complet est ici: http://www.godlikemouse.com/2008/03/04/optimizing-memcpy-routines/