Différences de performance entre les processus 32 et 64 bits
Question
Nous avons des machines Core2 (Dell T5400) avec XP64.
Nous observons que lors de l'exécution de processus 32 bits, la performance de memcpy est de l'ordre de 1,2 Go / s; mais memcpy dans un processus 64 bits atteint environ 2,2 Go / s (ou 2,4 Go / s avec le memcpy du compilateur Intel CRT). Tandis que le la réaction initiale pourrait être d'expliquer simplement cette loin en raison des registres plus larges disponibles dans le code 64 bits, nous observons que notre propre memcpy-like Code d'assemblage SSE (qui devrait utiliser 128 bits) charge large stocke indépendamment de 32/64-bitness de processus) démontre des limites supérieures similaires la bande passante de copie qu'il réalise.
Ma question est la suivante: quelle est cette différence? en raison de ? Les processus 32 bits doivent-ils passer au travers des cerceaux WOW64 supplémentaires pour accéder à la RAM? Est-ce quelque chose faire avec des TLB ou des prefetchers ou ... quoi?
Merci pour vos idées.
Également soulevé sur les forums Intel. .
La solution
Bien sûr, vous devez vraiment regarder les instructions de la machine qui sont exécutées dans la boucle la plus interne de la mémoire, en entrant dans le code de la machine avec un débogueur. Tout le reste n'est que spéculation.
Mon problème est que cela n’a probablement rien à voir avec le 32 bits par rapport au 64 bits en soi; Je suppose que la routine de bibliothèque la plus rapide a été écrite à l'aide de magasins SSE non temporels.
Si la boucle interne contient une variante des instructions conventionnelles load-store, ensuite, la mémoire de destination doit être lue dans le cache de la machine, modifiée et réécrite. Comme cette lecture est totalement inutile (les bits lus sont écrasés immédiatement), vous pouvez économiser la moitié de la bande passante mémoire en utilisant le & "Non-temporel &"; écrivez des instructions qui contournent les caches. De cette façon, la mémoire de destination est simplement écrite, ce qui en fait un aller simple dans la mémoire au lieu d’un aller-retour.
Je ne connais pas la bibliothèque CRT du compilateur Intel, c'est donc une supposition. Il n'y a pas de raison particulière pour laquelle la libCRT 32 bits ne peut pas faire la même chose, mais l'accélération que vous citez correspond à ce que je m'attendais à ce que je m'attende simplement en convertissant les instructions movdqa en ...
Puisque memcpy ne fait aucun calcul, il est toujours lié à la vitesse à laquelle vous pouvez lire et écrire de la mémoire.
Autres conseils
Je pense que ce qui suit peut l'expliquer:
Pour copier des données de la mémoire dans un registre et revenir à la mémoire, vous devez
mov eax, [address]
mov [address2], eax
Ceci déplace 32 bits (4 octets) d’adresse en adresse2. Il en va de même avec 64 bits en mode 64 bits
mov rax, [address]
mov [address2], rax
Ceci déplace 64 bits, 2 octets, d’adresse en adresse2. " mov " lui-même, qu’il s’agisse de 64 bits ou de 32 bits, a une latence de 0.5 et un débit de 0.5 conformément aux spécifications d’Intel. La latence correspond au nombre de cycles d'horloge nécessaires à une instruction pour parcourir le pipeline, tandis que le débit correspond à la durée pendant laquelle le processeur doit attendre avant d'accepter à nouveau la même instruction. Comme vous pouvez le constater, il peut effectuer deux mouvements par cycle d'horloge. Cependant, il doit attendre un demi-cycle d'horloge entre deux mouvements; il ne peut donc effectuer qu'un seul cycle par cycle d'horloge (ou ai-je tort ici et interprète-t-il mal les termes? Voir PDF ici pour plus de détails.
Bien sûr, un mov reg, mem
peut être plus long que 0,5 cycle, selon que les données sont dans le cache de niveau 1 ou 2, ou pas du tout dans le cache et doivent être extraites de la mémoire. Cependant, le temps de latence ci-dessus ignore ce fait (comme le PDF l'indique ci-dessus), il suppose que toutes les données nécessaires au mouvement sont déjà présentes (sinon, le temps de latence augmentera en fonction du temps nécessaire pour récupérer les données où qu'elles se trouvent. Pour le moment, il peut s’agir de plusieurs cycles d’horloge et est totalement indépendant de la commande en cours d’exécution, indique le fichier PDF, page 482 / C-30).
Ce qui est intéressant, que le mov soit 32 ou 64 bits ne joue aucun rôle. Cela signifie que si la largeur de bande de la mémoire ne devient pas le facteur limitant, les mouvements 64 bits sont également rapides que les 32 bits, et puisqu'il suffit de la moitié moins de mouvements pour déplacer la même quantité de données de A à B lors de l'utilisation de 64 bits, le débit peut (en théorie) deux fois plus élevé (le fait que ce n’est pas le cas, c’est probablement parce que la mémoire n’est pas rapide et illimitée).
D'accord, maintenant vous pensez qu'en utilisant les registres SSE plus grands, vous devriez obtenir un débit plus rapide, n'est-ce pas? Autant que je sache, les registres xmm ne sont pas 256, mais de 128 bits, BTW ( référence sur Wikipedia ). Cependant, avez-vous pris en compte la latence et le débit? Les données que vous souhaitez déplacer sont alignées sur 128 bits ou non. En fonction de cela, vous pouvez soit le déplacer en utilisant
movdqa xmm1, [address]
movdqa [address2], xmm1
ou si non aligné
movdqu xmm1, [address]
movdqu [address2], xmm1
Eh bien, movdqa / movdqu a une latence de 1 et un débit de 1. Ainsi, les instructions prennent deux fois plus de temps à être exécutées et le temps d'attente après les instructions est deux fois plus long qu'un mov normal.
Et nous n’avons même pas pris en compte le fait que la CPU divise les instructions en micro-opérations et qu’elle 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 charger des données dans / à partir de registres xmm est beaucoup plus lent que de charger des données dans / à partir de registres normaux, votre idée d'accélérer le transfert à l'aide de registres xmm a donc été vouée à l'échec dès la première seconde. En fait, je suis surpris de constater que, finalement, le MSE ne se déplace pas beaucoup plus lentement que la normale.
J'ai enfin compris tout ça (et la réponse de Die in Sente était sur la bonne voie, merci)
Dans ce qui suit, dst et src correspondent à 512 Mo stte :: vector. J'utilise le compilateur et le tube cathodique Intel 10.1.029.
à la fois sur 64 bits
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);
appeler
__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 à environ 2200 Mo / s.
Mais sur 32bit
_mm_stream_ps
appels
dst.size()
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 à environ 1350 Mo / s uniquement.
TOUTEFOIS
memcpy(&dst[0],&src[0],N)
où N est précédemment déclaré movnt
compile (sur 32 bits) en 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 à environ 2100 Mo / s (et prouver que 32 bits n’est pas limité par la bande passante).
Je retire ma plainte selon laquelle mon propre code SSE, semblable à celui de Memcpy, souffre d'un
similaire ~ 1300 MByte / limite dans les versions 32 bits; Je n'ai maintenant aucun problème
obtenir > 2GByte / s sur 32 ou 64 bits; l'astuce (comme l'indique les résultats ci-dessus)
consiste à utiliser des mémoires non temporelles (& "; streaming &";) (par exemple, CPUID
intrinsèques).
Cela semble un peu étrange que le " <=> " memcpy ne finit pas appeler le plus rapide " <=> " version (si vous entrez dans memcpy il y a le plus quantité incroyable de <=> logique de vérification et heuristique, par exemple, comparaison du nombre d'octets à copier avec la taille de la mémoire cache, etc. avant de passer à proximité de votre données réelles), mais au moins je comprends le comportement observé maintenant (et il est pas lié à SysWow64 ou H / W).
Mon hypothèse habituelle est que les processus 64 bits utilisent la taille de la 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 expliquer en partie ce qui se passe ici.
L'utilisation des mémoires non temporelles pour memcpy est certainement le à jeun rapide si vous ne faites que chronométrer l'appel memcpy.
D'autre part, si vous comparez une application, les magasins movdqa ont l'avantage de laisser la mémoire de destination dans le cache. Ou au moins la partie de celui-ci qui s'inscrit dans le cache.
Ainsi, si vous concevez une bibliothèque d'exécution et si vous pouvez supposer que l'application qui a appelé memcpy utilisera le tampon de destination immédiatement après l'appel de memcpy, vous souhaiterez fournir la version movdqa. Ceci optimise efficacement le trajet de la mémoire vers le processeur qui suivrait la version de movntdq, et toutes les instructions qui suivent l'appel suivront plus rapidement.
Par contre, si le tampon de destination est volumineux par rapport au cache du processeur, cette optimisation ne fonctionne pas et la version de movntdq vous donnerait des points de repère plus rapides pour vos applications.
Donc, l’idée de mémoire aurait plusieurs versions sous le capot. Lorsque le tampon de destination est petit par rapport au cache du processeur, utilisez movdqa, sinon, le tampon de destination est plus 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 les versions 32 bits et 64 bits.
Mon hypothèse est que la bibliothèque 64 bits n’est tout simplement pas aussi mature. Les développeurs ne sont pas encore prêts à fournir les deux routines dans cette version de la bibliothèque.
Je n'ai pas de référence sous les yeux, je ne suis donc pas absolument positif en ce qui concerne les horaires / instructions, mais je peux quand même donner la théorie. Si vous effectuez un déplacement de mémoire en mode 32 bits, vous ferez quelque chose comme un & "Rep movsd &"; qui déplace une seule valeur 32 bits à chaque cycle d'horloge. En mode 64 bits, vous pouvez effectuer une & "; Représentation movsq &"; qui fait 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 beaucoup simplifié, en ignorant tous les problèmes de bande passante mémoire / d’alignement, etc., mais c’est ici que tout commence ...
Voici un exemple de routine memcpy spécialement 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 /