Unten sind zwei Programme aufgeführt, die fast identisch sind, außer dass ich die Variablen i
und umgeschaltet habe j
. Sie laufen beide in unterschiedlicher Zeit. Könnte jemand erklären, warum dies passiert?
Version 1
#include <stdio.h>
#include <stdlib.h>
main () {
int i,j;
static int x[4000][4000];
for (i = 0; i < 4000; i++) {
for (j = 0; j < 4000; j++) {
x[j][i] = i + j; }
}
}
Version 2
#include <stdio.h>
#include <stdlib.h>
main () {
int i,j;
static int x[4000][4000];
for (j = 0; j < 4000; j++) {
for (i = 0; i < 4000; i++) {
x[j][i] = i + j; }
}
}
c
performance
for-loop
optimization
cpu-cache
Kennzeichen
quelle
quelle
Antworten:
Wie andere gesagt haben, ist das Problem das Speichern des Speicherorts im Array :
x[i][j]
. Hier ein kleiner Einblick, warum:Sie haben ein zweidimensionales Array, aber der Speicher im Computer ist von Natur aus eindimensional. Während Sie sich Ihr Array so vorstellen:
Ihr Computer speichert es als einzelne Zeile im Speicher:
Im zweiten Beispiel greifen Sie auf das Array zu, indem Sie zuerst die zweite Nummer durchlaufen, dh:
Das bedeutet, dass Sie sie alle in der richtigen Reihenfolge treffen. Schauen Sie sich jetzt die 1. Version an. Sie gehen:
Aufgrund der Art und Weise, wie C das 2D-Array im Speicher angeordnet hat, bitten Sie es, überall hin zu springen. Aber jetzt zum Kicker: Warum ist das wichtig? Alle Speicherzugriffe sind gleich, oder?
Nein, wegen Caches. Daten aus Ihrem Speicher werden in kleinen Blöcken (als "Cache-Zeilen" bezeichnet), normalerweise 64 Byte, an die CPU übertragen. Wenn Sie 4-Byte-Ganzzahlen haben, bedeutet dies, dass Sie 16 aufeinanderfolgende Ganzzahlen in einem ordentlichen kleinen Bündel erhalten. Es ist eigentlich ziemlich langsam, diese Erinnerungsstücke abzurufen. Ihre CPU kann in der Zeit, die zum Laden einer einzelnen Cache-Zeile benötigt wird, viel Arbeit leisten.
Schauen Sie sich jetzt die Reihenfolge der Zugriffe noch einmal an: Das zweite Beispiel besteht darin, (1) einen Teil von 16 Zoll zu greifen, (2) alle zu ändern, (3) 4000 * 4000/16 Mal zu wiederholen. Das ist schön und schnell und die CPU hat immer etwas zu arbeiten.
Das erste Beispiel ist (1) einen Teil von 16 Zoll nehmen, (2) nur einen davon modifizieren, (3) 4000 * 4000 Mal wiederholen. Das erfordert die 16-fache Anzahl von "Abrufen" aus dem Speicher. Ihre CPU muss tatsächlich Zeit damit verbringen, herumzusitzen und darauf zu warten, dass dieser Speicher angezeigt wird, und während sie herumsteht, verschwenden Sie wertvolle Zeit.
Wichtige Notiz:
Nachdem Sie die Antwort erhalten haben, ist hier ein interessanter Hinweis: Es gibt keinen inhärenten Grund, warum Ihr zweites Beispiel das schnelle sein muss. In Fortran wäre beispielsweise das erste Beispiel schnell und das zweite langsam. Das liegt daran, dass Fortran die Dinge nicht wie C in konzeptionelle "Zeilen" erweitert, sondern in "Spalten", dh:
Das Layout von C heißt 'Zeilenmajor' und Fortrans heißt 'Spaltenmajor'. Wie Sie sehen, ist es sehr wichtig zu wissen, ob Ihre Programmiersprache Zeilen- oder Spaltenmajor ist! Hier ist ein Link für weitere Informationen: http://en.wikipedia.org/wiki/Row-major_order
quelle
Nichts mit Montage zu tun. Dies ist auf Cache-Fehler zurückzuführen .
C mehrdimensionale Arrays werden mit der letzten Dimension als der schnellsten gespeichert. Die erste Version wird also bei jeder Iteration den Cache verpassen, während die zweite Version dies nicht tut. Die zweite Version sollte also wesentlich schneller sein.
Siehe auch: http://en.wikipedia.org/wiki/Loop_interchange .
quelle
Version 2 wird viel schneller ausgeführt, da der Cache Ihres Computers besser als Version 1 verwendet wird. Wenn Sie darüber nachdenken, sind Arrays nur zusammenhängende Speicherbereiche. Wenn Sie ein Element in einem Array anfordern, bringt Ihr Betriebssystem wahrscheinlich eine Speicherseite in den Cache, die dieses Element enthält. Da sich die nächsten Elemente jedoch auch auf dieser Seite befinden (weil sie zusammenhängend sind), befindet sich der nächste Zugriff bereits im Cache! Dies ist, was Version 2 tut, um die Geschwindigkeit zu erhöhen.
Version 1 hingegen greift spaltenweise und nicht zeilenweise auf Elemente zu. Diese Art des Zugriffs ist auf Speicherebene nicht zusammenhängend, sodass das Programm das Caching des Betriebssystems nicht so stark nutzen kann.
quelle
Der Grund ist der cache-lokale Datenzugriff. Im zweiten Programm scannen Sie linear durch den Speicher, was vom Caching und Prefetching profitiert. Das Speichernutzungsmuster Ihres ersten Programms ist weitaus weiter verteilt und weist daher ein schlechteres Cache-Verhalten auf.
quelle
Neben den anderen hervorragenden Antworten auf Cache-Treffer gibt es auch einen möglichen Optimierungsunterschied. Ihre zweite Schleife wird wahrscheinlich vom Compiler in etwas optimiert, das Folgendes entspricht:
Dies ist für die erste Schleife weniger wahrscheinlich, da der Zeiger "p" jedes Mal um 4000 erhöht werden müsste.
EDIT:
p++
und*p++ = ..
kann in den meisten CPUs sogar zu einem einzelnen CPU-Befehl kompiliert werden.*p = ..; p += 4000
kann nicht, daher ist es weniger vorteilhaft, es zu optimieren. Es ist auch schwieriger, weil der Compiler die Größe des inneren Arrays kennen und verwenden muss. Und es kommt nicht so häufig in der inneren Schleife im normalen Code vor (es tritt nur bei mehrdimensionalen Arrays auf, bei denen der letzte Index in der Schleife konstant gehalten wird und der vorletzte Schritt schrittweise ausgeführt wird), sodass die Optimierung weniger Priorität hat .quelle
p += 4000
isop++
i
wird bereits um einen Nicht-Einheitswert erhöht, da es sich um ein Zeigerinkrement handelt.int *f(int *p) { *p++ = 10; return p; } int *g(int *p) { *p = 10; p += 4000; return p; }
in gcc.godbolt.org einzugeben . Die beiden scheinen im Grunde das gleiche zu kompilieren.Diese Linie der Schuldige:
Die zweite Version verwendet kontinuierlichen Speicher und ist daher wesentlich schneller.
Ich habe es mit versucht
und die Ausführungszeit beträgt 13 Sekunden für Version 1 gegenüber 0,6 Sekunden für Version 2.
quelle
Ich versuche eine generische Antwort zu geben.
Weil
i[y][x]
es eine Abkürzung für*(i + y*array_width + x)
in C ist (probieren Sie das Noble ausint P[3]; 0[P] = 0xBEEF;
).Während Sie iterieren
y
, iterieren Sie über Größenblöckearray_width * sizeof(array_element)
. Wenn Sie das in Ihrer inneren Schleife haben, dann haben Siearray_width * array_height
Iterationen über diese Blöcke haben.Wenn Sie die Reihenfolge umdrehen, haben Sie nur
array_height
Chunk-Iterationen, und zwischen jeder Chunk-Iteration haben Sie nurarray_width
Iterationen vonsizeof(array_element)
.Während dies auf wirklich alten x86-CPUs nicht viel bedeutete, führt x86 heutzutage viel Prefetching und Caching von Daten durch. Sie erzeugen wahrscheinlich viele Cache-Fehler in Ihrer langsameren Iterationsreihenfolge.
quelle