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. .

Était-ce utile?

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 /

Licencié sous: CC-BY-SA avec attribution
Non affilié à StackOverflow
scroll top