Memcpy Leistungsunterschiede zwischen 32 und 64-Bit-Prozessen
Frage
Wir haben Core2 Maschinen (Dell T5400) mit XP64.
Wir beobachten, dass, wenn 32-Bit-Prozesse, die Leistung von memcpy ist in der Größenordnung von 1.2GByte / s; jedoch MEMCPY in einem 64-Bit-Prozess erreicht über 2.2GByte / s (oder 2.4GByte / s mit dem Intel-Compiler CRTs Memcpy). Während erste Reaktion könnte sein, nur das erklären weg, als aufgrund des breiteren Register verfügbar in 64-Bit-Code, beobachten wir, dass unsere eigenen Memcpy artigen SSE Assembler-Code (die 128-Bit verwenden sollten breite Last speichert unabhängig von 32/64-Bitwert von Das Verfahren) veranschaulicht, ähnlich Obergrenzen für die Kopie Bandbreite erreicht es.
Meine Frage ist, was dieser Unterschied ist eigentlich wegen? Do 32-Bit-Prozesse zu springen haben durch einige zusätzliche WOW64 Reifen im RAM zu bekommen? Ist es etwas, zu tun mit TLBs oder Prefetchers oder ... was?
Vielen Dank für jede Einsicht.
Auch auf Intel-Foren .
Lösung
Natürlich müssen Sie wirklich an den tatsächlichen Maschinenbefehlen suchen, die innerhalb der innersten Schleife des Memcpy ausgeführt werden, indem sie mit einem Debugger in den Maschinencode zu treten. Alles andere ist nur Spekulation.
Meine quess ist, dass es wahrscheinlich nichts hat, mit 32-Bit zu tun im Vergleich zu 64-Bit per se; Meine Vermutung ist, dass die schnellere Bibliotheksroutine geschrieben wurde SSE nicht-zeitliche Geschäfte verwendet wird.
Wenn die innere Schleife enthält jede Variation der herkömmlichen Lade-Speichere-Befehle, dann muss der Zielspeicher in die Maschine Cache, verändert und zurückgeschrieben ausgelesen werden. Da das Lese völlig unnötig ist - die Bits gelesen werden sofort überschrieben werden - Sie durch die Verwendung der „nicht-temporal“ Schreibbefehle die Hälfte der Speicherbandbreite sparen, die die Caches umgehen. Auf diese Weise wird der Zielspeicher nur geschrieben machte eine Einweg Reise in den Speicher statt einer Rundreise.
Ich weiß nicht, die CRT-Bibliothek von Intel Compiler, so ist dies nur eine Vermutung. Es gibt keinen besonderen Grund, warum die 32-Bit-libCRT nicht das gleiche tun können, aber die Beschleunigung Sie zitieren ist im Baseballstadion von dem, was ich durch Umwandlung der MOVDQA Anweisungen nur erwarten würde movnt ...
Da Memcpy wird keine Berechnungen zu tun, es ist immer gebunden, wie schnell können Sie lesen und Speicher schreiben.
Andere Tipps
Ich denke, folgende erklären kann:
Um die Daten aus dem Speicher in ein Register zu kopieren und zurück in den Speicher, was Sie tun
mov eax, [address]
mov [address2], eax
Dies bewegt 32 Bit (4 Byte) von Adresse Adresse 2. Das gleiche gilt mit 64 Bit in 64-Bit-Modus
mov rax, [address]
mov [address2], rax
Dies bewegt 64 Bit, 2 Byte, von Adresse zu address2. „Mov“ selbst, unabhängig davon, ob es sich um 64-Bit oder 32-Bit eine Latenzzeit von 0,5 und einen Durchsatz von 0,5 nach Intel-Spezifikationen hat. Die Latenz ist, wie viele Taktzyklen der Befehl durch die Pipeline zu erreichen und zum Durchsatz ist, wie lange die CPU warten muss, bevor er wieder den gleichen Befehl zu akzeptieren. Wie Sie sehen können, kann es zwei mov der pro Taktzyklus tun, ist es jedoch einen halben Taktzyklus zwischen zwei MOV warten muss, so kann es effektiv tun nur eine mov pro Taktzyklus (oder bin ich hier falsch und die Begriffe falsch interpretieren? siehe PDF hier für Details).
Natürlich ist ein mov reg, mem
kann mehr als 0,5 Zyklen sein, je nachdem, ob die Daten in der 1. oder 2. Level-Cache ist, oder nicht im Cache überhaupt und muss aus dem Speicher gepackt werden. Ignoriert jedoch die Latenzzeit von über diese Tatsache (als PDF-Staaten I oben verlinkten), nimmt er alle notwendigen Daten für die mov vorhanden sind bereits (sonst wird die Latenz von erhöhen, wie lange es dauert, um die Daten von holen, wo es ist jetzt - dies mehrere Taktzyklen sein könnte und ist völlig unabhängig von dem Befehl sagt auf Seite 482 / C-30)
Was ist interessant, ob die mov 32 oder 64 Bit spielt keine Rolle. Das bedeutet, es sei denn, die Speicherbandbreite der begrenzende Faktor wird, 64-Bit-MOVs sind gleich schnell auf 32 bit MOV, und da es nur halb so viele MOVs nimmt die gleiche Datenmenge von A zu bewegen, B, wenn 64-Bit verwendet wird, der Durchsatz (in der Theorie) doppelt so hoch sein (die Tatsache, dass es nicht wahrscheinlich, weil der Speicher nicht unbegrenzt schnell ist).
Okay, jetzt denken Sie, wenn die größeren SSE-Register verwenden, sollten Sie einen schnelleren Durchsatz, nicht wahr? AFAIK die XMM-Register sind nicht 256, sondern 128 Bit breit, BTW ( Verweis auf Wikipedia ). Sie haben jedoch Latenz und Durchsatz in Betracht gezogen? Entweder die Daten, die Sie verschieben möchten, ist 128 Bit ausgerichtet ist oder nicht. Abhängig davon, bewegen Sie sie entweder mit
movdqa xmm1, [address]
movdqa [address2], xmm1
oder, wenn nicht ausgerichtet
movdqu xmm1, [address]
movdqu [address2], xmm1
Nun, MOVDQA / movdqu hat eine Latenzzeit von 1 und einem Durchsatz von 1. So die Anweisungen nehmen doppelt so lang ausgeführt werden und die Wartezeit nach den Anweisungen ist doppelt so lang wie ein normaler mov.
Und noch etwas haben wir noch nicht einmal berücksichtigt, ist die Tatsache, dass die CPU tatsächlich teilt Befehle in Mikro-ops und es kann diese parallel ausführen. Jetzt beginnt es wirklich kompliziert zu werden ... noch zu kompliziert für mich.
Wie auch immer, ich weiß aus Erfahrung, Laden von Daten zu / von XMM-Registern ist viel langsamer als Laden von Daten in / aus dem normalen Register, so dass Ihre Idee Übertragung zu beschleunigen von XMM Registern wurde von der ersten Sekunde an zum Scheitern verurteilt. Ich bin eigentlich überrascht, dass die SSE memmove nicht viel langsamer als die normalen am Ende ist.
Ich habe endlich auf den Grund dieser (und Antwort in Sente des sterben war auf dem richtigen Weg, danke)
In der nachstehenden dst und src sind 512 MByte std :: vector. Ich bin mit dem Intel-Compiler 10.1.029 und CRT.
Auf 64-Bit beide
memcpy(&dst[0],&src[0],dst.size())
und
memcpy(&dst[0],&src[0],N)
wobei N vorher const size_t N=512*(1<<20);
deklariert
rufen
__intel_fast_memcpy
der Großteil davon besteht aus:
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
und läuft bei ~ 2200 MByte / s.
Aber auf 32bit
memcpy(&dst[0],&src[0],dst.size())
Anrufe
__intel_fast_memcpy
der Großteil davon besteht aus
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
und läuft bei ~ 1350 MByte / s nur.
JEDOCH
memcpy(&dst[0],&src[0],N)
wobei N vorher const size_t N=512*(1<<20);
compiles deklariert wird (auf 32-Bit) auf einen direkten Aufruf zu einem
__intel_VEC_memcpy
der Großteil davon besteht aus
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)
und läuft bei ~ 2100MByte / s (und 32bit erweist sich irgendwie nicht begrenzte Bandbreite).
hebe ich meine Behauptung, dass mein eigener Memcpy artige SSE-Code aus einer leidet
ähnlich ~ 1300 MByte / Limit in 32bit baut; Ich jetzt habe keine Probleme
immer> 2 GByte / s auf 32 oder 64 Bit; der Trick (wie die obigen Ergebnisse deuten)
ist nicht-zeitliche ( "Streaming") speichert zu verwenden (z _mm_stream_ps
intrinsisch).
Es scheint ein wenig seltsam, dass die 32-Bit „dst.size()
“ Memcpy nicht schließlich
rufen Sie die schnellen „movnt
“ Version (wenn Sie in Memcpy Schritt gibt es die meisten
unglaubliche Menge an CPUID
Prüfung und heuristischer Logik z Anzahlvergleich
von Bytes mit Cache-Größe usw. kopiert werden, bevor es irgendwo in der Nähe Ihrer geht
Ist-Daten), aber zumindest verstehe ich das beobachtete Verhalten jetzt (und es ist
nicht SysWow64 oder H / W bezogen).
Meine off-the-Manschette Vermutung ist, dass die 64-Bit-Prozesse des Prozessors native 64-Bit-Speichergröße verwenden, die die Verwendung des Speicherbusses optimiert.
Vielen Dank für das positive Feedback! Ich glaube, ich kann teilweise erklären, was hier vor sich geht.
, um die nicht-zeitlichen Geschäfte für memcpy verwenden ist definitiv das gefastet , wenn Sie nur den Memcpy Anruf Timing.
Auf der anderen Seite, wenn Sie eine Anwendung Benchmarking hat die MOVDQA speichern den Vorteil, dass sie den Zielspeicher im Cache lassen. Oder zumindest der Teil davon, die in der Cache paßt.
Wenn Sie also eine Laufzeitbibliothek sind die Gestaltung und wenn Sie davon ausgehen können, dass die Anwendung, die Memcpy genannt wird den Zielpuffer unmittelbar nach dem Memcpy Aufruf verwenden, dann werden Sie die MOVDQA Version zur Verfügung stellen mögen. Dies optimiert heraus effektiv die Reise aus dem Speicher zurück in die CPU, die die movntdq Version folgen würde, und alle Anweisungen des Anrufs folgende laufen schneller.
Aber auf der anderen Seite, wenn der Zielpuffer groß ist im Vergleich zu dem Cache des Prozessors, ist, dass die Optimierung nicht funktioniert und die movntdq Version würden Ihnen schneller Anwendungsbenchmarks geben.
So ist die Idee Memcpy mehrere Versionen unter der Haube hat. Wenn der Zielpuffer an den Cache des Prozessors klein im Vergleich ist, verwenden MOVDQA, sonst, so ist der Zielpuffer groß im Vergleich zu dem Cache des Prozessors, Verwendung movntdq. Es klingt wie das ist, was in der 32-Bit-Bibliothek passiert.
Natürlich nichts davon etwas mit den Unterschieden zwischen dem 32-Bit- und 64-Bit zu tun.
Meine Vermutung ist, dass die 64-Bit-Bibliothek gerade nicht so ausgereift ist. Die Entwickler haben einfach nicht dazu gekommen, doch beide Routinen in dieser Version der Bibliothek zu bieten.
Ich habe keine Referenz vor mir, also bin ich nicht unbedingt positiv auf die Timings / Anweisungen, aber ich kann immer noch die Theorie geben. Wenn Sie einen Speicher bewegen unter 32-Bit-Modus zu tun, werden Sie so etwas wie ein „rep movsd“ tun, die in jedem Taktzyklus einen einzelnen 32-Bit-Wert bewegt. Unter 64-Bit-Modus können Sie eine „rep movsq“ tun, die sich ein einzelner 64-Bit in jedem Taktzyklus bewegen. Diese Anweisung ist auf 32-Bit-Code nicht verfügbar ist, so würde man 2 x rep movsd tun (bei 1 Zyklus eines Stück) für die Hälfte der Ausführungsgeschwindigkeit.
sehr vereinfacht, ohne auf alle Speicherbandbreite / Ausrichtungsprobleme, usw., aber das ist, wo alles beginnt ...
Hier ist ein Beispiel für eine Memcpy Routine speziell für 64-Bit-Architektur ausgerichtet.
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()
Der vollständige Artikel ist hier: http://www.godlikemouse.com/2008/03/04/ Optimierung-memcpy-Routinen /