2013-10-27 5 views
17

Ich experimentierte mit AVX-AVX2-Befehlssätzen, um die Leistung des Streamens auf aufeinanderfolgenden Arrays zu sehen. Also habe ich unten ein Beispiel, wo ich Grundspeicher lese und speicher.Haswell-Speicherzugriff

#include <iostream> 
#include <string.h> 
#include <immintrin.h> 
#include <chrono> 
const uint64_t BENCHMARK_SIZE = 5000; 

typedef struct alignas(32) data_t { 
    double a[BENCHMARK_SIZE]; 
    double c[BENCHMARK_SIZE]; 
    alignas(32) double b[BENCHMARK_SIZE]; 
} 
data; 

int main() { 
    data myData; 
    memset(&myData, 0, sizeof(data_t)); 

    auto start = std::chrono::high_resolution_clock::now(); 

    for (auto i = 0; i < std::micro::den; i++) { 
    for (uint64_t i = 0; i < BENCHMARK_SIZE; i += 1) { 
     myData.b[i] = myData.a[i] + 1; 
    } 
    } 
    auto end = std::chrono::high_resolution_clock::now(); 
    std::cout << (end - start).count()/std::micro::den << " " << myData.b[1] 
      << std::endl; 
} 

Und nachdem er mit Kompilieren g ++ - 4.9 -ggdb -march = Kern-AVX2 -std = C++ 11 struct_of_arrays.cpp O3 -o struct_of_arrays

I pro Zyklus Leistung recht guten Unterricht zu sehen und Timings, für Benchmark-Größe 4000. Aber sobald ich die Benchmark-Größe auf 5000 erhöhen, sehe ich, dass der Befehl pro Zyklus deutlich sinkt und auch Latenzsprünge. Jetzt ist meine Frage, obwohl ich sehen kann, dass Leistungseinbußen scheint mit L1-Cache verwandt zu sein, kann ich nicht erklären, warum dies so plötzlich passiert.

Um mehr Einblick zu geben, wenn ich mit Benchmark Größe 4000 perf laufen und 5000

| Event        | Size=4000 | Size=5000 | 
|-------------------------------------+-----------+-----------| 
| Time        | 245 ns | 950 ns | 
| L1 load hit       | 525881 | 527210 | 
| L1 Load miss      |  16689 |  21331 | 
| L1D writebacks that access L2 cache | 1172328 | 623710387 | 
| L1D Data line replacements   | 1423213 | 624753092 | 

So ist meine Frage, warum diese Auswirkungen geschehen, haswell Erwägung ziehen, soll auf der Bereitstellung von 2 * 32 Byte der Lage sein, lesen, und 32 Bytes speichern jeden Zyklus?

EDIT 1

ich mit diesem Code gcc realisiert intelligent beseitigt Zugriffe auf die myData.a, da es auf 0 gesetzt ist dies zu vermeiden Ich habe eine andere Benchmark, die etwas anders ist, wo ein explizit gesetzt .

Im zweiten Beispiel wird ein Array gelesen und ein anderes Array geschrieben. Und dieser produziert für verschiedene Größen perf Ausgabe folgende:

| Event   | Size=1000 | Size=2000 | Size=3000 | Size=4000  | 
|----------------+-------------+-------------+-------------+---------------| 
| Time   | 86 ns  | 166 ns  | 734 ns  | 931 ns  | 
| L1 load hit | 252,807,410 | 494,765,803 | 9,335,692 | 9,878,121  | 
| L1 load miss | 24,931  | 585,891  | 370,834,983 | 495,678,895 | 
| L2 load hit | 16,274  | 361,196  | 371,128,643 | 495,554,002 | 
| L2 load miss | 9,589  | 11,586  | 18,240  | 40,147  | 
| L1D wb acc. L2 | 9,121  | 771,073  | 374,957,848 | 500,066,160 | 
| L1D repl.  | 19,335  | 1,834,100 | 751,189,826 | 1,000,053,544 | 

wieder gleiche Muster in der Antwort wie erwähnt zu sehen sind, mit Datensatz Größendaten zu erhöhen paßt nicht mehr in L1 und L2 wird Engpass. Was ist auch interessant ist, dass Prefetching scheint nicht zu helfen und L1 vermisst erhöht erheblich. Obwohl ich erwarte, eine Trefferrate von mindestens 50 Prozent zu sehen, wenn man bedenkt, dass jede in L1 zum Lesen eingelesene Cachezeile ein Treffer für den zweiten Zugriff ist (64 Byte Cachezeile 32 Byte wird mit jeder Iteration gelesen). Sobald der Datensatz jedoch auf L2 übertragen wurde, scheint die L1-Trefferrate auf 2% gefallen zu sein. Wenn man bedenkt, dass Arrays sich nicht wirklich mit der L1-Cache-Größe überschneiden, sollte dies nicht an Cache-Konflikten liegen. Also macht dieser Teil immer noch keinen Sinn für mich.

Antwort

18

Zusammenfassung:
Unterschiedliche Cache-Ebene unterschiedliche Spitzenbandbreiten für die gleiche grundlegende Arbeitsbelastung aushalten können, so unterschiedlich große Datensatz die Leistung erheblich beeinflussen können.

längere Erklärung:
Es ist nicht sehr überraschend, bedenkt, dass Haswell, nach this article für z.B.

kann

2 Belastungen aufnehmen und 1 Speicher pro Zyklus

aber das gesagt, nur für die L1 anzuwenden. Wenn Sie lesen sehen Sie, dass die L2

eine vollständige 64B Linie auf die Daten oder Befehlscache

jedem Zyklus zur Verfügung stellen kann

Da Sie benötigen eine Last und ein Speicher pro Iteration, Wenn der Datensatz in L1 liegt, können Sie die L1-Bandbreite genießen und möglicherweise einen Zyklus-pro-Iteration-Durchsatz erreichen, während das Überlaufen des Datensatzes auf L2 Sie dazu zwingen würde, länger zu warten. Dies hängt davon ab, wie groß Double in Ihrem System ist, aber Ihre Ergebnisse zeigen an, dass es wahrscheinlich 32bit ist, also 4000 * 2 Arrays * 4 Byte = 32k, genau die L1-Größe und 5000 darüber hinaus.

Nun gibt es zwei Dinge, die passieren, wenn Sie mehr als in die nächste Cache-Ebene beginnen:

  1. L1-Rückschreiben: Beachten Sie, dass der Artikel nicht Rückschreiben nicht erwähnt, sind eine zusätzliche Strafe Sie haben in der Bandbreite zu zahlen (wie aus Ihrer Perf-Ausgabe zu sehen - obwohl es ein bisschen steil aussieht). Wenn man die Daten in der L1 hält, bedeutet das, dass man keine Räumung vornehmen muss, während einige Daten in L2 bedeuten, dass jede von L2 gelesene Zeile eine existierende Linie von der L1 werfen müsste, von der die Hälfte modifiziert wird Ihr Code und erfordern explizite Writebacks. Diese Transaktionen müssten zusätzlich zum Lesen der Werte für die zwei Datenelemente, die Sie pro Iteration verwenden, hinzukommen. Denken Sie daran, dass der Speicher auch zuerst die alten Daten lesen muss, da ein Teil der Zeile nicht verwendet wird und zusammengeführt werden muss.

  2. Cache-Ersetzungsstrategie - beachten Sie, dass, da der Cache-assoziativ gesetzt ist und höchstwahrscheinlich ein LRU-Schema verwendet, und da Sie Ihre Arrays gehen über in Reihe, würde Ihr Cache Nutzungsmuster wahrscheinlich die erste assoziative Weise füllt, dann Weiter zum zweiten Weg usw. - wenn Sie den letzten Weg füllen, werden, wenn noch Daten in der L2 benötigt werden (im Fall des größeren Datensatzes), Sie wahrscheinlich alle Linien seit dem ersten Weg vertreiben Sie sind die am wenigsten genutzten, auch wenn das bedeutet, dass sie als nächstes verwendet werden. Das ist der Nachteil von LRU mit größeren Datenmengen als der Cache.

Dies erklärt, warum in der Leistung der Rückgang so plötzlich ist aufgrund dieser Zugriffsmuster, wenn Sie die Cache-Größe von mindestens die Größe einer einzelnen Art und Weise (1/8th der L1-Cache) nicht überschreiten.

Ein letzter Kommentar zu den Perf-Ergebnissen - Sie hätten erwartet, dass die L1-Trefferrate für den 5000-Elemente-Fall auf eine schöne runde Null fallen würde, was ich glaube. Der HW-Prefetching kann es jedoch so aussehen lassen, als ob Sie ihn immer noch in der L1 treffen, da er vor den eigentlichen Datenlesevorgängen läuft. Sie müssen immer noch auf diese Vorabrufe warten, um die Daten zu übertragen, und noch wichtiger, da Sie die Bandbreite messen - sie nehmen immer noch die gleiche Bandbreite wie tatsächliche Ladevorgänge/Speicher, aber sie werden nicht von perf berücksichtigt, was Sie glauben lässt Du hattest die ganze Zeit L1-Treffer. Das ist zumindest meine beste Schätzung - Sie könnten das überprüfen, indem Sie die Vorabrufe deaktivieren und erneut messen (ich scheine diesen Rat zu oft zu geben, tut mir leid, dass ich so ein Hemmschuh bin).


EDIT 1 (nach Ihrem)

großer Fang über das eliminierte Array, das das Geheimnis über die doppelte Größe löst - es ist in der Tat 64 Bit, also entweder eine Anordnung von 4000 Elementen oder 2-Arrays Jeweils 2000 Elemente (nach deinem Fix) sind so viel wie du in die L1 passen kannst. Jetzt tritt das Verschütten bei 3000 Elementen auf. Die L1-Trefferrate ist jetzt niedrig, da L1 nicht genügend Vorabrufe ausgeben konnte, um vor Ihren 2 verschiedenen Streams zu laufen.

Für die Erwartung, dass jede Last eine 64-Byte-Linie für 2 Iterationen bringen würde - ich sehe etwas ziemlich interessant - wenn Sie die Anzahl der Lasten aus der Speichereinheit (L1 trifft + L1 fehlt) summieren Ich werde sehen, dass der 2000 Elemente Fall ist fast genau 2x von den 1000 Elementen, aber die 3000 und 4000 Fälle sind nicht 3x und 4x, sondern eher die Hälfte. Insbesondere haben Sie mit 3000 Elementen pro Array weniger Zugriffe als mit 2000 Elementen!
Das lässt mich vermuten, dass die Speichereinheit in der Lage ist, jeweils zwei Lasten in einen einzigen Speicherzugriff zusammenzuführen, aber nur, wenn man auf L2 und darüber hinaus geht. Das macht Sinn, wenn Sie daran denken, dass es keinen Grund gibt, einen weiteren Zugang zum Nachschlagen des L2 zu vergeben, wenn Sie bereits einen für diese Leitung haben, und es ist ein praktikabler Weg, die niedrigere Bandbreite auf dieser Ebene zu mindern. Ich vermute, dass aus irgendeinem Grund die zweite Ladung nicht einmal dann als eine L1-Lookup gezählt wird, und hilft nicht die Trefferquote, die Sie sehen wollten (Sie könnten die Zähler überprüfen, wie viele Lasten die Ausführung übergeben - das sollte wahrscheinlich wahr sein). Dies ist nur eine Ahnung, ich bin mir nicht sicher, wie der Zähler definiert ist, aber es entspricht der Anzahl der Zugriffe, die wir sehen.

+1

+1. Das einzige, was ich hinzufügen würde ist, dass auf jeder x86-Plattform, die ich gesehen habe, ein Double ist 8 Bytes. –

+0

In der Tat haben Sie Recht mit Rückschreiben und wie sie Bandbreite verbrauchen, wenn sie nicht in L1 sind. Es ist enttäuschend, die Leistungsfähigkeit der Verarbeitungseinheit nicht nutzen zu können, wenn sich die Daten nicht in L1 befinden (was fast immer bei jedem Streaming-Anwendungsfall der Fall ist, der größer als L1 ist). – edorado

+1

Aus diesem Grund teilen leistungskritische Algorithmen ihre Arbeitssätze oft in Teilmengen auf, die in die kleineren Caches passen (siehe z. B. Cache-Tiling-Techniken). Laut Artikel L2 wurde auch die Bandbreite im Vergleich zu älteren CPUs erhöht, ich denke, es ist nur schwer, die L1-Verbesserungen nachzuholen. – Leeor