it-swarm.com.de

Warum sinkt die Geschwindigkeit von memcpy () alle 4 KB drastisch?

Ich habe die Geschwindigkeit von memcpy() getestet, als ich feststellte, dass die Geschwindigkeit bei i * 4KB drastisch abfällt. Das Ergebnis ist wie folgt: Die Y-Achse ist die Geschwindigkeit (MB/Sekunde) und die X-Achse die Puffergröße für memcpy(), die von 1 KB auf 2 MB ansteigt. Subfigure 2 und Subfigure 3 beschreiben den Teil von 1KB-150KB und 1KB-32KB.

Umgebung:

CPU: Intel (R) Xeon (R) CPU E5620 bei 2.40 GHz 

OS: 2.6.35-22-generisches # 33-Ubuntu

GCC-Compilerflags: -O3 -msse4 -DINTEL_SSE4 -Wall -std = c99

Graphs of memcpy speed showing troughs every 4k

Ich denke, es muss sich auf Caches beziehen, aber ich kann keinen Grund aus den folgenden Cache-unfreundlichen Fällen finden:

Da die Leistungseinbußen dieser beiden Fälle durch unfreundliche Schleifen verursacht werden, die gestreute Bytes in den Cache lesen, wird der Rest des Platzes einer Cachezeile verschwendet.

Hier ist mein Code:

void memcpy_speed(unsigned long buf_size, unsigned long iters){
    struct timeval start,  end;
    unsigned char * pbuff_1;
    unsigned char * pbuff_2;

    pbuff_1 = malloc(buf_size);
    pbuff_2 = malloc(buf_size);

    gettimeofday(&start, NULL);
    for(int i = 0; i < iters; ++i){
        memcpy(pbuff_2, pbuff_1, buf_size);
    }   
    gettimeofday(&end, NULL);
    printf("%5.3f\n", ((buf_size*iters)/(1.024*1.024))/((end.tv_sec - \
    start.tv_sec)*1000*1000+(end.tv_usec - start.tv_usec)));
    free(pbuff_1);
    free(pbuff_2);
}

AKTUALISIEREN

Angesichts der Vorschläge von @usr, @ChrisW und @Leeor habe ich den Test genauer überarbeitet und die untenstehende Grafik zeigt die Ergebnisse. Die Puffergröße reicht von 26 KB bis 38 KB, und ich habe alle 64B (26 KB, 26 KB, 64 KB, 26 KB, 128 KB, ..., 38 KB) getestet. Jeder Test wiederholt sich 100.000 Mal in etwa 0,15 Sekunden. Das Interessante ist, dass der Abfall nicht nur exakt in der 4-KB-Grenze auftritt, sondern auch in 4 * i + 2 KB mit einer weitaus geringeren Amplitudenabnahme auftritt.

More graphs showing performance drops

PS

@Leeor bot eine Möglichkeit, den Drop zu füllen, und fügte einen 2-KB-Dummy-Puffer zwischen pbuff_1 und pbuff_2 hinzu. Es funktioniert, aber ich bin nicht sicher über Leeors Erklärung.

enter image description here

50
foool

Der Speicher ist normalerweise in 4-KB-Seiten organisiert (obwohl auch größere Formate unterstützt werden). Der virtuelle Adressraum, den Ihr Programm sieht, ist möglicherweise zusammenhängend, im physischen Speicher jedoch nicht unbedingt. Das Betriebssystem, das eine Zuordnung von virtuellen zu physischen Adressen (in der Seitenzuordnung) verwaltet, versucht normalerweise, die physischen Seiten zusammenzuhalten. Dies ist jedoch nicht immer möglich und kann zu Brüchen führen (insbesondere bei längerer Verwendung, wenn sie gelegentlich ausgetauscht werden) ).

Wenn Ihr Speicherstrom eine Grenze von 4k Seiten überschreitet, muss die CPU anhalten und eine neue Übersetzung abrufen. Wenn sie die Seite bereits gesehen hat, wird sie möglicherweise im TLB zwischengespeichert, und der Zugriff wird so optimiert, dass er am schnellsten ist ist der erste Zugriff (oder wenn Sie zu viele Seiten haben, auf denen sich die TLBs festhalten können), muss die CPU den Speicherzugriff anhalten und einen Seitenrundgang über die Seitenzuordnungseinträge starten - das ist relativ lang, da jede Ebene tatsächlich ist Ein Speicher, der von selbst gelesen wird (auf virtuellen Maschinen ist er sogar noch länger, da jede Ebene möglicherweise einen vollständigen Seitenpfad auf dem Host benötigt).

Ihre memcpy-Funktion hat möglicherweise ein anderes Problem: Beim erstmaligen Zuweisen von Speicher erstellt das Betriebssystem die Seiten nur in der Pagemap, markiert sie jedoch aufgrund interner Optimierungen als nicht zugegriffen und nicht geändert. Der erste Zugriff ruft möglicherweise nicht nur einen Seitenübergang auf, sondern auch eine Unterstützung, die dem Betriebssystem mitteilt, dass die Seite verwendet (und für die Zielpufferseiten gespeichert) wird, was einen teuren Übergang zu einem Betriebssystemhandler erfordern würde.

Um dieses Rauschen zu beseitigen, ordnen Sie die Puffer einmal zu, wiederholen Sie die Kopie mehrmals und berechnen Sie die Amortisationszeit. Auf der anderen Seite würde dies zu einer "warmen" Leistung führen (d. H. Nachdem die Caches aufgewärmt wurden), sodass die Cachegrößen in Ihren Diagrammen angezeigt werden. Wenn Sie einen "kalten" Effekt erzielen möchten, ohne an Paging-Latenzen zu leiden, sollten Sie die Caches zwischen den Iterationen leeren (stellen Sie nur sicher, dass Sie dies nicht zeitlich festlegen).

BEARBEITEN

Lesen Sie die Frage noch einmal durch und Sie scheinen eine korrekte Messung durchzuführen. Das Problem mit meiner Erklärung ist, dass es nach 4k*i allmählich zunehmen sollte, da Sie bei jedem solchen Abwurf die Strafe erneut zahlen, dann aber die freie Fahrt bis zu den nächsten 4 km genießen sollten. Es erklärt nicht, warum es solche "Spitzen" gibt, und danach kehrt die Geschwindigkeit zur Normalität zurück.

Ich denke, Sie haben ein ähnliches Problem wie das in Ihrer Frage verknüpfte kritische Problem: Wenn Ihre Puffergröße eine schöne runde 4k ist, werden beide Puffer ausgerichtet zu den gleichen Sätzen im Cache und sich gegenseitig verprügeln. Ihr L1 ist 32 KB groß, es scheint also zunächst kein Problem zu sein. Wenn Sie jedoch davon ausgehen, dass es sich bei den L1-Daten um 8 KB handelt, handelt es sich in der Tat um einen 4-KB-Wrap-Around für dieselben Mengen, und Sie haben 2 * 4-KB-Blöcke mit genau derselben Ausrichtung (unter der Annahme, dass die Zuordnung zusammenhängend erfolgt ist), so dass sie sich auf denselben Sätzen überlappen. Es reicht aus, dass das LRU nicht genau so funktioniert, wie Sie es erwarten, und dass es weiterhin zu Konflikten kommt.

Um dies zu überprüfen, würde ich versuchen, einen Dummy-Puffer zwischen pbuff_1 und pbuff_2 zu malloc, machen Sie es 2k groß und hoffen, dass es die Ausrichtung bricht.

EDIT2:

Ok, da dies funktioniert, ist es Zeit, ein wenig auszuarbeiten. Angenommen, Sie weisen zwei 4k-Arrays in den Bereichen 0x1000-0x1fff und 0x2000-0x2fff zu. Set 0 in Ihrem L1 enthält die Zeilen bei 0x1000 und 0x2000, Set 1 enthält 0x1040 und 0x2040 und so weiter. Bei diesen Größen gibt es noch keine Probleme mit Thrashing. Sie können alle nebeneinander existieren, ohne die Assoziativität des Caches zu beeinträchtigen. Jedes Mal, wenn Sie eine Iteration durchführen, haben Sie jedoch eine Last und einen Speicher, die auf denselben Satz zugreifen - ich vermute, dies kann einen Konflikt in der Hardware verursachen. Schlimmer noch - Sie benötigen mehrere Iterationen, um eine einzelne Zeile zu kopieren. Dies bedeutet, dass Sie eine Überlastung von 8 Ladevorgängen + 8 Speichern haben (weniger, wenn Sie vektorisieren, aber immer noch viel), die alle auf dieselbe schlechte Menge abzielen. Ich bin hübsch Sicher, es gibt eine Menge Kollisionen, die sich dort verstecken.

Ich sehe auch, dass Intel Optimization Guide speziell dazu etwas zu sagen hat (siehe 3.6.8.2):

4-KByte-Speicher-Aliasing tritt auf, wenn der Code auf zwei verschiedene Speicherorte mit einem 4-KByte-Versatz dazwischen zugreift. Die 4-KByte-Aliasing-Situation kann sich in einer Speicherkopierroutine manifestieren, in der die Adressen des Quellpuffers und des Zielpuffers einen konstanten Versatz beibehalten und der konstante Versatz zufällig ein Vielfaches des Byte-Inkrements von einer Iteration zur nächsten ist.

...

ladungen müssen warten, bis die Filialen stillgelegt wurden, bevor sie fortgesetzt werden können. Beispiel: Bei Offset 16 ist die Last des nächsten Iterationsspeichers mit 4 KByte Alias. Daher muss die Schleife warten, bis der Speichervorgang abgeschlossen ist, wodurch die gesamte Schleife serialisiert wird. Die Wartezeit verringert sich mit einem größeren Versatz, bis der Versatz 96 das Problem behebt (da zum Zeitpunkt des Ladens mit derselben Adresse keine ausstehenden Speicher vorhanden sind).

30
Leeor

Ich erwarte, dass es so ist:

  • Wenn die Blockgröße ein Vielfaches von 4 KB ist, ordnet malloc neue Seiten aus dem O/S zu.
  • Wenn die Blockgröße kein 4-KB-Vielfaches ist, ordnet malloc einen Bereich von seinem (bereits zugewiesenen) Heap zu.
  • Wenn die Seiten vom O/S zugewiesen werden, sind sie "kalt": Das erste Mal zu berühren, ist sehr teuer.

Meine Vermutung ist, dass, wenn Sie eine einzelne memcpy vor der ersten gettimeofday ausführen, der zugewiesene Speicher "aufgewärmt" wird und Sie dieses Problem nicht sehen werden. Anstelle eines ersten Memcpy reicht es möglicherweise sogar aus, ein Byte in jede zugewiesene 4-KB-Seite zu schreiben, um die Seite vorzuwärmen.

Normalerweise, wenn ich einen Leistungstest wie den Ihren will, codiere ich ihn wie folgt:

// Run in once to pre-warm the cache
runTest();
// Repeat 
startTimer();
for (int i = count; i; --i)
  runTest();
stopTimer();

// use a larger count if the duration is less than a few seconds
// repeat test 3 times to ensure that results are consistent
2
ChrisW

Da Sie sich oft in einer Schleife befinden, sind Argumente für nicht zugeordnete Seiten irrelevant. Meines Erachtens ist der Effekt, dass der Hardware-Prefetcher nicht bereit ist, die Seitengrenze zu überschreiten, um (möglicherweise unnötige) Seitenfehler nicht zu verursachen.

0
virco