Ich untersuche Leistungs-Hotspots in einer Anwendung, die 50 % ihrer Zeit in memmove(3) verbringt. Die Anwendung fügt Millionen von 4-Byte-Ganzzahlen in sortierte Arrays ein und verwendet memmove, um die Daten “nach rechts” zu verschieben, um Platz für den eingefügten Wert zu schaffen.
Meine Erwartung war, dass das Kopieren des Speichers extrem schnell ist, und ich war überrascht, dass so viel Zeit in memmove investiert wird. Aber dann hatte ich die Idee, dass memmove langsam ist, weil es überlappende Regionen verschiebt, die in einer engen Schleife implementiert werden müssen, anstatt große Speicherseiten zu kopieren. Ich habe einen kleinen Mikrobenchmark geschrieben, um herauszufinden, ob es einen Leistungsunterschied zwischen memcpy und memmove gibt, in der Erwartung, dass memcpy zweifellos gewinnen wird.
Ich habe meinen Benchmark auf zwei Rechnern (Core i5, Core i7) laufen lassen und gesehen, dass memmove tatsächlich schneller ist als memcpy, auf dem älteren Core i7 sogar fast doppelt so schnell! Jetzt suche ich nach Erklärungen.
Hier ist mein Maßstab. Es kopiert 100 MB mit memcpy und bewegt sich dann um etwa 100 MB mit memmove; Quelle und Ziel überschneiden sich. Es werden verschiedene “Entfernungen” für Quelle und Ziel ausprobiert. Jeder Test wird 10 Mal durchgeführt, die durchschnittliche Zeit wird ausgedruckt.
https://gist.github.com/cruppstahl/78a57cdf937bca3d062c
Hier sind die Ergebnisse auf dem Core i5 (Linux 3.5.0-54-generic #81~precise1-Ubuntu SMP x86_64 GNU/Linux, gcc ist 4.6.3 (Ubuntu/Linaro 4.6.3-1ubuntu5). Die Zahl in Klammern ist der Abstand (Lückengröße) zwischen Quelle und Ziel:
memcpy 0.0140074
memmove (002) 0.0106168
memmove (004) 0.01065
memmove (008) 0.0107917
memmove (016) 0.0107319
memmove (032) 0.0106724
memmove (064) 0.0106821
memmove (128) 0.0110633
Memmove ist als SSE-optimierter Assembler-Code implementiert, der von hinten nach vorne kopiert. Es verwendet Hardware-Prefetch, um die Daten in den Cache zu laden, kopiert 128 Bytes in XMM-Register und speichert sie dann am Ziel.
(memcpy-ssse3-back.SZeilen 1650 ff)
L(gobble_ll_loop):
prefetchnta -0x1c0(%rsi)
prefetchnta -0x280(%rsi)
prefetchnta -0x1c0(%rdi)
prefetchnta -0x280(%rdi)
sub $0x80, %rdx
movdqu -0x10(%rsi), %xmm1
movdqu -0x20(%rsi), %xmm2
movdqu -0x30(%rsi), %xmm3
movdqu -0x40(%rsi), %xmm4
movdqu -0x50(%rsi), %xmm5
movdqu -0x60(%rsi), %xmm6
movdqu -0x70(%rsi), %xmm7
movdqu -0x80(%rsi), %xmm8
movdqa %xmm1, -0x10(%rdi)
movdqa %xmm2, -0x20(%rdi)
movdqa %xmm3, -0x30(%rdi)
movdqa %xmm4, -0x40(%rdi)
movdqa %xmm5, -0x50(%rdi)
movdqa %xmm6, -0x60(%rdi)
movdqa %xmm7, -0x70(%rdi)
movdqa %xmm8, -0x80(%rdi)
lea -0x80(%rsi), %rsi
lea -0x80(%rdi), %rdi
jae L(gobble_ll_loop)
Warum ist memmove schneller als memcpy? Ich würde erwarten, dass memcpy Speicherseiten kopiert, was viel schneller sein sollte als das Schleifen. Im schlimmsten Fall würde ich erwarten, dass memcpy so schnell ist wie memmove.
PS: Ich weiß, dass ich memmove in meinem Code nicht durch memcpy ersetzen kann. Ich weiß, dass das Codebeispiel C und C++ mischt. Diese Frage ist wirklich nur für akademische Zwecke.
AKTUALISIERUNG 1
Ich habe einige Variationen der Tests durchgeführt, basierend auf den verschiedenen Antworten.
- Wenn memcpy zweimal ausgeführt wird, ist der zweite Durchlauf schneller als der erste.
- Beim “Berühren” des Zielpuffers von memcpy (
memset(b2, 0, BUFFERSIZE...)
) dann ist der erste Lauf von memcpy auch schneller. - memcpy ist immer noch etwas langsamer als memmove.
Hier sind die Ergebnisse:
memcpy 0.0118526
memcpy 0.0119105
memmove (002) 0.0108151
memmove (004) 0.0107122
memmove (008) 0.0107262
memmove (016) 0.0108555
memmove (032) 0.0107171
memmove (064) 0.0106437
memmove (128) 0.0106648
Meine Schlussfolgerung: Basierend auf einem Kommentar von @Oliver Charlesworth muss das Betriebssystem physischen Speicher festschreiben, sobald zum ersten Mal auf den Memcpy-Zielpuffer zugegriffen wird (wenn jemand weiß, wie man dies “beweist”, dann fügen Sie bitte eine Antwort hinzu! ). Darüber hinaus ist memmove, wie @Mats Petersson sagte, Cache-freundlicher als memcpy.
Danke für all die tollen Antworten und Kommentare!
Sie haben sich den Memmove-Code angesehen, haben Sie sich auch den Memcpy-Code angesehen?
– Oliver Charlesworth
20. Februar 2015 um 7:56 Uhr
Meine Erwartung war, dass das Kopieren von Speicher extrem schnell ist – nur wenn sich Speicher im L1-Cache befindet. Wenn die Daten nicht in Caches passen, schwindet Ihre Kopierleistung.
– Maxim Egorushkin
20. Februar 2015 um 8:19 Uhr
Übrigens, Sie haben nur einen Zweig von kopiert
memmove
. Dieser Zweig kann keine Verschiebung verarbeiten, wenn die Quelle das Ziel überlappt und sich das Ziel an niedrigeren Adressen befindet.– Maxim Egorushkin
20. Februar 2015 um 8:23 Uhr
Ich hatte noch keine Zeit, auf einen Linux-Rechner zuzugreifen, daher kann ich diese Theorie noch nicht testen. Aber eine andere mögliche Erklärung ist Überschuldung; dein
memcpy
Schleife ist das erste Mal, dass der Inhalt vonb2
zugegriffen wird, daher muss das Betriebssystem während des Vorgangs physischen Speicher dafür bereitstellen.– Oliver Charlesworth
20. Februar 2015 um 8:26 Uhr
PS: Wenn dies ein Engpass ist, würde ich den Ansatz überdenken. Wie wäre es, wenn Sie die Werte in eine Liste oder Baumstruktur (z. B. Binärbaum) packen und am Ende in ein Array einlesen. Die Knoten in einem solchen Ansatz wären ein hervorragender Kandidat für die Pool-Zuweisung. Sie werden nur bis zum Ende hinzugefügt, wenn sie massenhaft veröffentlicht werden. Das gilt insbesondere, wenn Sie wissen, wie viele Sie am Anfang benötigen. Die Boost-Bibliotheken haben einen Pool-Zuordner.
– Hartnäckig
20. Februar 2015 um 8:48 Uhr