it-swarm.com.de

Warum ist memmove schneller als memcpy?

Ich untersuche Performance-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.

Ich hatte erwartet, dass das Kopieren von Speicher extrem schnell ist, und ich war überrascht, dass so viel Zeit in memmove verbracht 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 damit gerechnet, dass memcpy zweifellos gewinnt.

Ich habe meinen Benchmark auf zwei Rechnern (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 Maßstab. 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 ~ exact1-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 wird 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 als das Schleifen sein sollte. 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.

  1. Wenn Sie memcpy zweimal ausführen, ist der zweite Lauf schneller als der erste.
  2. Wenn der Zielpuffer von memcpy (memset(b2, 0, BUFFERSIZE...)) "berührt" wird, ist auch der erste Durchlauf von memcpy schneller.
  3. memcpy ist 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 den physischen Speicher festschreiben, sobald das erste Mal auf den Speicherzielpuffer zugegriffen wird (wenn jemand weiß, wie man dies "prüft", dann fügen Sie bitte eine Antwort hinzu! ). Außerdem ist memmove, wie @Mats Petersson sagte, cachefreundlicher als memcpy.

Vielen Dank für all die tollen Antworten und Kommentare!

87
cruppstahl

Ihre memmove Aufrufe verschieben den Speicher um 2 bis 128 Bytes, während Ihre memcpy Quelle und Ihr Ziel völlig unterschiedlich sind. Irgendwie ist das der Grund für den Leistungsunterschied: Wenn Sie an die gleiche Stelle kopieren, werden Sie feststellen, dass memcpy möglicherweise schneller ausfällt, z. am ideone.com :

memmove (002) 0.0610362
memmove (004) 0.0554264
memmove (008) 0.0575859
memmove (016) 0.057326
memmove (032) 0.0583542
memmove (064) 0.0561934
memmove (128) 0.0549391
memcpy 0.0537919

Kaum etwas dran - kein Hinweis darauf, dass das Zurückschreiben auf eine bereits fehlerhafte Speicherseite große Auswirkungen hat, und wir sehen mit Sicherheit keine Halbierung von Zeit ... aber es zeigt, dass es nichts Falsches gibt, was memcpy unnötig langsamer macht, wenn man Äpfel für Äpfel vergleicht.

55
Tony Delroy

Wenn Sie memcpy verwenden, müssen die Schreibvorgänge in den Cache verschoben werden. Wenn Sie memmove verwenden, während Sie einen kleinen Schritt vorwärts kopieren, befindet sich der zu kopierende Speicher bereits im Cache (da er 2, 4, 16 oder 128 Byte "zurück" gelesen wurde). Versuchen Sie es mit einem memmove, bei dem das Ziel mehrere Megabyte (> 4 * Cachegröße) beträgt, und ich vermute, dass Sie ähnliche Ergebnisse erzielen werden (aber ich kann mich nicht darum kümmern, dies zu testen).

Ich garantiere, dass es bei ALLEN um die Cache-Wartung geht, wenn Sie große Speicheroperationen ausführen.

22
Mats Petersson

In der Vergangenheit haben memmove und memcopy dieselbe Funktion. Sie arbeiteten auf die gleiche Weise und hatten die gleiche Implementierung. Es wurde dann erkannt, dass memcopy nicht definiert werden muss (und häufig nicht definiert wurde), um überlappende Bereiche auf eine bestimmte Art und Weise zu behandeln.

Das Endergebnis ist, dass memmove so definiert wurde, dass überlappende Bereiche auf bestimmte Weise behandelt werden, auch wenn dies die Leistung beeinträchtigt. Memcopy soll den besten verfügbaren Algorithmus für nicht überlappende Regionen verwenden. Die Implementierungen sind normalerweise fast identisch.

Das Problem, auf das Sie gestoßen sind, ist, dass es so viele Variationen der x86-Hardware gibt, dass es unmöglich ist zu sagen, welche Methode zum Verschieben des Speichers am schnellsten ist. Und selbst wenn Sie glauben, unter bestimmten Umständen ein Ergebnis zu haben, kann ein so einfacher Schritt im Speicherlayout zu einer sehr unterschiedlichen Cache-Leistung führen.

Sie können entweder das Benchmarking durchführen oder das Problem ignorieren und sich auf die Benchmarks für die C-Bibliothek verlassen.

Edit: Oh, und noch eine letzte Sache; Das Verschieben vieler Speicherinhalte ist SEHR langsam. Ich würde vermuten, dass Ihre Anwendung mit so etwas wie einer einfachen B-Tree-Implementierung schneller läuft, um Ihre ganzen Zahlen zu verarbeiten. (Oh du bist, okay)

Edit2: Um meine Erweiterung in den Kommentaren zusammenzufassen: Das Mikrobenchmark ist das Problem hier, es misst nicht, was Sie denken, dass es ist. Die Aufgaben, die memcpy und memmove übertragen werden, unterscheiden sich erheblich voneinander. Wenn die memcpy zugewiesene Aufgabe mit memmove oder memcpy mehrmals wiederholt wird, hängt das Endergebnis nicht davon ab, welche Memory Shifting-Funktion Sie verwenden, WENN sich die Regionen nicht überschneiden.

15
user3710044

"memcpy ist effizienter als memmove." In Ihrem Fall tun Sie höchstwahrscheinlich nicht genau dasselbe, während Sie die beiden Funktionen ausführen.

Im Allgemeinen wird USE memmove nur dann verwendet, wenn dies erforderlich ist. VERWENDEN Sie es, wenn die Wahrscheinlichkeit sehr hoch ist, dass sich die Quell- und Zielregionen überlappen.

Referenz: https://www.youtube.com/watch?v=Yr1YnOVG-4g Dr. Jerry Cain, (Stanford Intro Systems Lecture - 7) Zeit: 36:00

2
Ehsan