Ich untersuche Leistungs-Hotspots in einer Anwendung, die 50% ihrer Zeit in memmove verbringt (3). Die Anwendung fügt Millionen von 4-Byte-Ganzzahlen in sortierte Arrays ein und verschiebt die Daten mithilfe von memmove "nach rechts", um Platz für den eingefügten Wert zu schaffen.
Meine Erwartung war, dass das Kopieren von Speicher extrem schnell ist, und ich war überrascht, dass so viel Zeit für memmove aufgewendet wird. Aber dann kam mir die Idee, dass memmove langsam ist, weil es überlappende Bereiche verschiebt, die in einer engen Schleife implementiert werden müssen, anstatt große Speicherseiten zu kopieren. Ich habe ein kleines Mikrobenchmark geschrieben, um herauszufinden, ob es einen Leistungsunterschied zwischen memcpy und memmove gibt, und erwartet, dass memcpy zweifellos gewinnt.
Ich habe meinen Benchmark auf zwei Computern (Core i5, Core i7) ausgeführt und festgestellt, dass memmove tatsächlich schneller als memcpy ist, auf dem älteren Core i7 sogar fast doppelt so schnell! Jetzt suche ich nach Erklärungen.
Hier ist mein Benchmark. Es kopiert 100 MB mit memcpy und bewegt sich dann mit memmove um 100 MB. Quelle und Ziel überschneiden sich. Es werden verschiedene "Entfernungen" für Quelle und Ziel versucht. Jeder Test wird 10 Mal ausgeführt, die durchschnittliche Zeit wird gedruckt.
https://gist.github.com/cruppstahl/78a57cdf937bca3d062c
Hier sind die Ergebnisse auf dem Core i5 (Linux 3.5.0-54-generisch # 81 ~ präzise1-Ubuntu SMP x86_64 GNU / Linux, gcc ist 4.6.3 (Ubuntu / Linaro 4.6.3-1ubuntu5). Die Zahl in Klammern ist die Entfernung (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.S , Zeilen 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.
UPDATE 1
Ich habe einige Variationen der Tests durchgeführt, basierend auf den verschiedenen Antworten.
- Wenn Sie memcpy zweimal ausführen, ist der zweite Lauf schneller als der erste.
- Wenn Sie den Zielpuffer von memcpy (
memset(b2, 0, BUFFERSIZE...)
) "berühren", ist auch der erste Durchlauf von memcpy 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
Mein Fazit: 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", fügen Sie bitte eine Antwort hinzu! ). Darüber hinaus ist memmove, wie @Mats Petersson sagte, cachefreundlicher als memcpy.
Vielen Dank für all die tollen Antworten und Kommentare!
memmove
. Dieser Zweig kann keine Verschiebung verarbeiten, wenn die Quelle das Ziel überlappt und sich das Ziel an niedrigeren Adressen befindet.
memcpy
Schleife wird zum ersten Mal b2
zugegriffen, wenn auf deren Inhalt zugegriffen wird. Daher muss das Betriebssystem im Laufe der Zeit physischen Speicher dafür bereitstellen.