memcpy und compare mit SSE
-
Hallo,
da ich eine schnellstmögliche Vergleichsfunktion, sowie eine MemCpy-Prozedur brauche, wollte ich ein paar Tests mit Hilfe von SSE-Befehlen starten, da das SIMD-Prinzip dafür ja sehr viel versprechend klingt.
Da mir zugegebenermaßen noch nicht ganz klar ist, wie ich bei all den Vergleichsfunktionen das Ergebnis prüfe (Flags werden ja nicht gesetzt und alles was ich gelesen habe, war mir zu schleierhaft), habe ich mich erst mal auf die Internet-Recherche zur MemCpy-Funktion begeben, um einen Eindruck davon zu erhalten, was es da schon gibt.
Ichc bin u.a. auf diesen Code gestoßen:
void X_aligned_memcpy_sse2(void* dest, const void* src, const unsigned long size_t) { __asm { mov esi, src; //src pointer mov edi, dest; //dest pointer mov ebx, size_t; //ebx is our counter shr ebx, 7; //divide by 128 (8 * 128bit registers) loop_copy: prefetchnta 128[ESI]; //SSE2 prefetch prefetchnta 160[ESI]; prefetchnta 192[ESI]; prefetchnta 224[ESI]; movdqa xmm0, 0[ESI]; //move data from src to registers movdqa xmm1, 16[ESI]; movdqa xmm2, 32[ESI]; movdqa xmm3, 48[ESI]; movdqa xmm4, 64[ESI]; movdqa xmm5, 80[ESI]; movdqa xmm6, 96[ESI]; movdqa xmm7, 112[ESI]; movntdq 0[EDI], xmm0; //move data from registers to dest movntdq 16[EDI], xmm1; movntdq 32[EDI], xmm2; movntdq 48[EDI], xmm3; movntdq 64[EDI], xmm4; movntdq 80[EDI], xmm5; movntdq 96[EDI], xmm6; movntdq 112[EDI], xmm7; add esi, 128; add edi, 128; dec ebx; jnz loop_copy; //loop please loop_copy_end: } }
Wenn ich das korrekt sehe, müsste Src hier 128 Byte(!) aligned sein, damit sich der Aufruf lohnen würde. In anderen Fällen kann es aus meiner Sicht sogar zu Zugriffsverletzungen kommen, weshalb ich die Funktion nicht wirklich gut finde.
Ich habe mal ein kleines Test-Szenario aufgebaut:
int main() { void *src = _aligned_malloc(200, 0), *dest = _aligned_malloc(200, 0); DWORD dwStart = GetTickCount(); for( unsigned i = 0; i < (1 << 31); i++ ) { // memcpy(dest, src, 200); // aligned_memcpy(src, dest, 200); // X_aligned_memcpy_sse2(dest, src, 200); } cout << "Benoetigte Zeit:\t" << (GetTickCount() - dwStart) / 1000 << endl; _aligned_free(src); _aligned_free(dest); }
Meine Resultate waren:
memcpy: 61 Sekunden X_aligned_memcpy_sse2: 296 Sekunden aligned_memcpy: 294 Sekunden
aligned_memcpy ist eine eigen Implementation von mir; mit wenig Erfolg wie man sieht.
void aligned_memcpy( void* src, void* dest, size_t count ) { __asm { [asm] mov esi, src mov edi, dest mov ecx, count mov edx, count shr ecx, 4 // divide by 16 (Byte = 128 Bit) jnz SHORT begin_128 end_128: mov ecx, edx shr ecx, 3 // divide by 8 (Byte = 64 Bit) jnz SHORT begin_64 end_64: mov ecx, edx shr ecx, 2 // divide by 4 (Byte = 32 Bit) jnz SHORT begin_32 end_32: mov ecx, edx and ecx, 3 // prepare byte copy jnz SHORT begin_byte jmp SHORT done begin_128: prefetchnta [esi+16] movdqa xmm0, [esi] movntdq [edi], xmm0 add esi, 16 add edi, 16 sub edx, 16 dec ecx jnz SHORT begin_128 jmp SHORT end_128 begin_64: prefetchnta [esi+8] movq mm0, [esi] movntq [edi], mm0 add esi, 8 add edi, 8 sub edx, 8 dec ecx jnz SHORT begin_64 jmp SHORT end_64 begin_32: repnz movsd jmp SHORT end_32 begin_byte: repnz movsb done: //ret [/asm]} }
Soll das heißen, dass SSE für eine Kopierfunktion einfach nicht geeignet ist oder mache ich hier einfach grundsätzlich was falsch?
-
Ich habe das gerade mal bei mir (Core 2 Duo E8500) ausprobiert und das was die Funktionen langsam macht ist das movntdq. Nimmt man stattdessen auch movdqa hat man so ziemlich genau die Geschwindigkeit des normalen memcpy. Allerdings kann man sich so den Cache zumüllen. Das kommt in dem Testprogramm aber nicht rüber.
www.agner.org/optimize/asmlib.zip enthält auch eine Implementierung für memcpy mit SSE und eine PDF die ein paar Details und Performancetests dazu gibt. Demnach lohnt sich das ganze nur wenn die Quelle und Ziel nicht aligned sind.
-
Tobiking2 schrieb:
Ich habe das gerade mal bei mir (Core 2 Duo E8500) ausprobiert und das was die Funktionen langsam macht ist das movntdq. Nimmt man stattdessen auch movdqa hat man so ziemlich genau die Geschwindigkeit des normalen memcpy. Allerdings kann man sich so den Cache zumüllen. Das kommt in dem Testprogramm aber nicht rüber.
Hi,
genau das habe ich im Laufe des Vormittags auch herausgefunden. Tatsächlich erreicht man (fast) exakt die selben Zeiten, wenn man das "prefetchnta" und das "movntdq" herausnimmt, wie das normale memcpy.Mit dem oben gezeigten Code ist es ca. 3-4x langsamer. Irgendwie fehlt mir da noch das Verständnis führ. Ich ging zu Beginn eigentlich davon aus, dass gerade das Prefetching (macht zu gegeben auch nur den Bruchteil der Verzögerung aus) und vorallem das movntdq am Cache vorbei, richtig schnell sein müsste.
Da jede Speicherstelle nur ein mal angepackt wird, brauche ich die Daten nämlich nicht im Cache.
Man sollte doch meinen, dass das zusätzliche verarbeiten im Cache (inkl. Verdrängung) mehr Zeit in Anspruch nehmen müsste.Dass das Ganze unaligned schneller sein soll, kann ich mir fast nicht vorstellen. Müsste doch umgekehrt sein.
Alles in allem ziemlich enttäuschendes Resultat. Allerdings benutzt auch das standard memcpy die Funktion _VEC_memcpy, insofern mehr als 255 Byte kopiert werden. Sollte auch die ungefähr gleichen Zeiten erklären.
-
http://www.amd.com/us-en/assets/content_type/white_papers_and_tech_docs/22007.pdf
da gibt es ne schnelle mecpy implementierung.
von amd gab es mal ein paper zur memcpy optimierung, angefangen beim simplen c for-loop bis zu der version da im paper, leider finde ich das optimierungspaper gerade nicht.
-
Die Prefetchdistanz dürfte viel zu klein sein, um nützlich zu sein. Innerhalb der der gleichen Speicherseite greift einerseits normalerweise ohnehin Hardware-Prefetching ein, andererseits ist anderfalls die Latenz des Hauptspeichers wahrscheinlich zu groß, um die Daten noch rechtzeitig lesen zu können.
-
camper schrieb:
Die Prefetchdistanz dürfte viel zu klein sein, um nützlich zu sein. Innerhalb der der gleichen Speicherseite greift einerseits normalerweise ohnehin Hardware-Prefetching ein, andererseits ist anderfalls die Latenz des Hauptspeichers wahrscheinlich zu groß, um die Daten noch rechtzeitig lesen zu können.
Ja, das stimmt. Zudem bekomme ich bei meiner Zeitmessung natürlich auch nicht unbedingt die Zeit mit, in der die Daten wirklich in den Arbeitsspeicher geschrieben werden.
Das Schreiben in den Cache ist natürlich schneller.Ich weiß nicht wie eure Erfahrungen sind. Aber sowohl auf einem Core 2 Duo, als auch Core 2 Quad, ist die Benutzung der XMM-Register u. 128 Bit Operationen deutlich langsamer, als wenn man die MMX-Register benutzt und 64 bitweise kopiert.
EDIT:
Ich kann erst morgen die Performance auf einem AMD-Rechner testen, jedoch ist die selbige auf meinen Core 2 Quad, Core i7 und Core 2 Duo Rechern die schlechteste von allen getesteten Verfahren, bei Speicherblöcken zwischen 16 u. 256 Byte, aligned und unaligned.
-
FrEEzE2046 schrieb:
Mit dem oben gezeigten Code ist es ca. 3-4x langsamer. Irgendwie fehlt mir da noch das Verständnis führ. Ich ging zu Beginn eigentlich davon aus, dass gerade das Prefetching (macht zu gegeben auch nur den Bruchteil der Verzögerung aus) und vorallem das movntdq am Cache vorbei, richtig schnell sein müsste.
Da jede Speicherstelle nur ein mal angepackt wird, brauche ich die Daten nämlich nicht im Cache.
Man sollte doch meinen, dass das zusätzliche verarbeiten im Cache (inkl. Verdrängung) mehr Zeit in Anspruch nehmen müsste.Der Cache ist kein Overhead. Der Sinn vom Kopieren am Cache vorbei ist wie bereits gesagt, keine Daten aus dem Cache zu verdrängen. Deswegen wäre ein besserer Benchmark einer, der abwechselnd kopiert und komplexe Operationen auf Daten im Cache ausführt (komplexer als memcpy und so).
-
Solche Untersuchungen haben schon andere vorher gemacht: A survey about state of the art C compiler optimization tricks.
Ich weiß nicht wie eure Erfahrungen sind.
Der Compiler kann es (meist) besser!
Deswegen wäre ein besserer Benchmark einer, der ...
Nein, einfach den erzeugten Assemblercode fuer die Plattform betrachten und Takte gemaess den Instruktionen zaehlen. Man will ja nicht die Cacheperformance testen, sonder die Optimierungsmoeglichkeiten von Compilern.
-
knivil schrieb:
Nein, einfach den erzeugten Assemblercode fuer die Plattform betrachten und Takte gemaess den Instruktionen zaehlen. Man will ja nicht die Cacheperformance testen, sonder die Optimierungsmoeglichkeiten von Compilern.
Oh, wenn das so ist, muss ich was überlesen haben ...
-
Ich hab zu diesem Thema kürzlich auch Messungen auf meinem Rechner durchgeführt, unter der Nutzung von _mm_stream_si128.
Habe das Test-Programm "CacheAndMemory" von AMD angepaßt.http://forum.ultimatespaceproject.de/medien/screenshots/EventPerf2k10.png
-
Grundsätzlich hängt es nach meinen Messungen stark vom verwendeten Prozessor ab. Man sollte also für ein optimales Ergebnis mehrere Funktionen für jeden Fall zur Verfügung haben.
Das ganze kann ja über eine JumpTable ganz zu beginn (evtl. beim Laden einer DLL) als Initialisierung überprüft werden.Ganz nebenbei ist momentan movdq halt noch sehr langsam. DAher sollte man auch unter Verwendung der XMM Register weiterhin movq benutzen. Mal schauen wie's bei AVX aussieht.