Warum ist das transponieren einer matrix von 512x512 viel langsamer als transponieren einer matrix von 513x513?
Nach der Durchführung einiger Experimente, die auf quadratische Matrizen in verschiedenen Größen, ein Muster kam. Immer, transponieren einer matrix der Größe 2^n
langsamer ist als die Umsetzung einer Größe 2^n+1
. Für kleine Werte von n
, der Unterschied ist nicht bedeutend.
Große Unterschiede auftreten, jedoch über einen Wert von 512. (zumindest für mich)
Disclaimer: ich kenne die Funktion tatsächlich nicht transponieren der matrix wegen der doppelten vertauschen von Elementen, aber es macht keinen Unterschied.
Folgt der code:
#define SAMPLES 1000
#define MATSIZE 512
#include <time.h>
#include <iostream>
int mat[MATSIZE][MATSIZE];
void transpose()
{
for ( int i = 0 ; i < MATSIZE ; i++ )
for ( int j = 0 ; j < MATSIZE ; j++ )
{
int aux = mat[i][j];
mat[i][j] = mat[j][i];
mat[j][i] = aux;
}
}
int main()
{
//initialize matrix
for ( int i = 0 ; i < MATSIZE ; i++ )
for ( int j = 0 ; j < MATSIZE ; j++ )
mat[i][j] = i+j;
int t = clock();
for ( int i = 0 ; i < SAMPLES ; i++ )
transpose();
int elapsed = clock() - t;
std::cout << "Average for a matrix of " << MATSIZE << ": " << elapsed / SAMPLES;
}
Ändern MATSIZE
können uns ändern die Größe (duh!). Ich veröffentlichte zwei Versionen auf ideone:
- Größe 512 - Durchschnitt 2.46 ms - http://ideone.com/1PV7m
- Größe 513 - Durchschnitt 0.75 ms - http://ideone.com/NShpo
In meiner Umgebung (MSVS 2010 -, full-Optimierungen), der Unterschied ist ähnlich :
- Größe 512 - Durchschnitt 2.19 ms
- Größe 513 - Durchschnitt 0.57 ms
Warum ist das passiert?
- Dein code sieht cache-unfreundlich zu mir.
- und es ist.
- Es ist so ziemlich das gleiche Problem wie in dieser Frage: stackoverflow.com/questions/7905760/...
- Pflege zu elavorate, @CodesInChaos? (Oder sonst jemand.)
- Wie zu Lesen, die akzeptierte Antwort?
- Ja, ich sah, was Sie meinte. Ich lese in Kommentaren zuerst.
- Ich habe versucht den code mit gcc 4.6.1 in ubuntu 11.10 mit der Standard-Optimierung(O0-Niveau), 513*513 matrix ist langsamer als die 512*512 ein,(2.62 ms für die Größe 513, und 2,45 ms für Größe 512), mit Optimierung-O1 oder oben, Zeit für 512 2.08 ms, und für die 513 ist 0,56 ms. cpu: Intel(R) Core(TM) i3 530 2.93 GHz. kann das jemand erklären?
- Es ist ein bisschen sinnlos zu Messen, alles ohne Optimierungen. Mit Optimierungen deaktiviert ist, wird der generierte code wird übersät sein mit den nebensächlichen Müll, verstecken sich andere Engpässe. (z.B. Speicher)
Du musst angemeldet sein, um einen Kommentar abzugeben.
Die Erklärung kommt von Agner Fog in Optimierung von software in C++ und es reduziert, wie die Daten abgerufen und im cache gespeichert.
Begriffen und detaillierte Informationen finden Sie in der wiki-Eintrag auf caching, ich werde verengen es hier.
Einem cache organisiert ist, in setzt und Linien. In einer Zeit, die nur verwendet wird, von denen eine der Zeilen enthält, kann verwendet werden. Der Speicher kann eine Zeile Spiegel mal die Anzahl der Zeilen gibt uns der cache-Größe.
Für einen bestimmten Speicher-Adresse, können wir berechnen, welche Menge Spiegel mit der Formel:
Diese Art von Formel, die im Idealfall gibt es eine gleichmäßige Verteilung über die Gruppen, weil jeder Speicher-Adresse ist, wie wahrscheinlich zu Lesen (sagte ich ideal).
Es ist klar, dass überschneidungen auftreten können. Im Falle eines cache-miss, wird der Speicher in die lese-cache-und der alte Wert ersetzt. Erinnern jeder Satz hat eine Reihe von Linien, aus denen die least-recently-used ist überschrieben mit dem neu-lese-Speicher.
Ich werde versuchen, etwas Folgen dem Beispiel von Agner:
Davon ausgehen, dass jedes set hat 4 Zeilen, die jeweils 64 Byte. Wir den ersten Versuch zum Lesen der Adresse
0x2710
, das geht in Satz28
. Und dann haben wir auch versucht zu Lesen, Adressen0x2F00
,0x3700
,0x3F00
und0x4700
. Alle diese gehören zu der gleichen Reihe. Vor dem Lesen0x4700
sämtliche Linien, die in dem set gewesen wäre, besetzt. Lesung, der Speicher evicts eine vorhandene Zeile in der Menge, die Linie, die zunächst hielt0x2710
. Das problem liegt in der Tatsache, dass wir Lesen von Adressen, die (für dieses Beispiel)0x800
auseinander. Dies ist die kritischen Schritt (wieder für das Beispiel).Den kritischen Schritt kann auch berechnet werden:
Variablen Abstand
criticalStride
oder ein Vielfaches auseinander, kämpfen für die gleiche cache-Zeilen.Dies ist der Theorie-Teil. Neben der Erklärung (auch Agner, ich verfolge es eng zu vermeiden, Fehler zu machen):
Davon aus einer matrix von 64x64 (denken Sie daran, die Wirkungen variieren je nach cache) mit 8kb cache, 4 Zeilen pro set * Größe von 64 bytes. Jede Zeile kann bis zu 8-von den Elementen in der matrix (64-bit
int
).Den kritischen Schritt wäre, 2048 bytes, das entspricht 4 Zeilen der matrix (die kontinuierliche in-memory).
Übernehmen wir die Verarbeitung Zeile 28. Wir versuchen, nehmen Sie die Elemente dieser Zeile und tauschen Sie sich mit den Elementen aus Spalte 28. Die ersten 8 Glieder der Reihe bilden eine cache-line, aber Sie gehen in 8 verschiedenen cache-Zeilen in Spalte 28. Denken Sie daran, kritische Schritt 4 Zeilen auseinander (4 aufeinander folgende Elemente in einer Spalte).
Wenn das element 16 erreicht ist, in der Spalte (4 cache-lines pro set & 4 Reihen auseinander = Probleme), die ex-0-element wird aus dem cache verdrängten. Wenn wir erreichen das Ende der Spalte alle vorherigen cache-Zeilen hätte verloren und musste Neuladen auf den Zugriff auf das nächste element (die gesamte Zeile wird überschrieben).
Haben eine Größe, die nicht ein Vielfaches der kritische Schritt, Saut diese perfekte Szenario für eine Katastrophe, wie wir sind, nicht mehr den Umgang mit Elementen, die wichtig sind, Schrittlänge voneinander entfernt auf die vertikale, also die Anzahl der cache neu geladen wird stark reduziert.
Anderen Haftungsausschluss - ich habe gerade meinen Kopf um die Erklärung und hoffe, dass ich nagelte ihn, aber ich könnte falsch sein. Wie auch immer, ich bin warten auf eine Antwort (oder Bestätigung) von Mysticial. 🙂
Intel core i3
pc aufUbuntu 11.04 i386
zeigt fast die gleiche Leistung, die mit gcc-4.6.Und so ist das gleiche für meinen computerIntel Core 2 Duo
mit mingw-gcc4.4,der läuft aufwindows 7(32)
.Es zeigt einen großen Unterschied, wenn ich kompilieren diesem segment mit einer etwas älteren pcintel centrino
mit gcc-4.6,der läuft aufubuntu 12.04 i386
.which goes in set 24
meinst du "im set 28" statt? Und tun Sie übernehmen die 32-sets?Luchian gibt eine Erklärung warum dieses Verhalten passiert, aber ich dachte, es wäre eine nette Idee, um zu zeigen, eine mögliche Lösung für dieses problem und gleichzeitig zeigen Sie ein wenig über cache-oblivious-algorithmen.
Ihren Algorithmus im Grunde genommen:
ist einfach nur schrecklich für eine moderne CPU. Eine Lösung ist, um zu wissen, die details über Ihre cache-system und die Anpassung des Algorithmus zu vermeiden diese Probleme. Funktioniert sehr gut, solange Sie wissen, diese details.. nicht besonders portable.
Können wir besser machen? Ja, wir können: Einen Allgemeinen Ansatz für dieses problem sind cache-oblivious-algorithmen, die, wie der name schon sagt, vermeidet es, abhängig von der spezifischen cache-Größen [1]
Die Lösung würde wie folgt Aussehen:
Etwas komplexer, aber ein kurzer test zeigt etwas sehr Interessantes auf meinem alten e8400 mit VS2010 x64 Version, testcode für
MATSIZE 8192
Edit: Über den Einfluss der Größe: Es ist viel weniger ausgeprägt ist zwar noch spürbar um einige Grad, das ist, weil wir mit der iterativen Lösung als Blattknoten statt recursing bis zu 1 (die übliche Optimierung für rekursive algorithmen). Wenn wir LEAFSIZE = 1, wird der cache hat keinen Einfluss für mich [
8193: 1214.06; 8192: 1171.62ms, 8191: 1351.07ms
- das ist innerhalb der Fehlergrenze, die Schwankungen sind im 100ms-Bereich, das "benchmark" ist nicht etwas, dass ich zu bequem mit, wenn wir wollten, ganz genaue Werte])[1] Quellen für diese Sachen: Gut, wenn Sie nicht bekommen kann einen Vortrag von jemandem, der arbeitete mit Leiserson und co auf dieser.. ich nehme an, Ihre Papiere ein guter Ausgangspunkt. Diese algorithmen sind noch Recht selten beschrieben - CLR hat eine einzige Fußnote über Sie. Noch, es ist eine großartige Möglichkeit, um die Leute zu überraschen.
Bearbeiten (Hinweis: ich bin nicht derjenige, der dies geschrieben, Antwort; ich wollte nur hinzufügen das):
Hier ist eine komplette C++ - version des obigen Codes:
LEAFSIZE
Konstante bedeutet, mein Algorithmus ist nicht vollständig cache-oblivious (aber offensichtlich schneller als recursing bis zu 1), so würde ich erwarten, dass einige kleine Wirkung. Die Erhöhung der leafsize sollten diesen Effekt noch dramatischer, aber, da am Ende würden wir implementieren Sie die iterative Lösung. Ich dachte nur, ein Algorithmus, der wäre nicht/viel weniger beeinflusst durch caching wäre eine gute Passform für diese Frage als Nachtrag zu Ihrer Antwort.recursiveTranspose
hat, dh, dass es nicht gefüllt wird, bis der cache so viel durch den Betrieb auf kleine Kacheln (vonLEAFSIZE x LEAFSIZE
dimension).LEAFSIZE
ist ein Kritischer Schritt.Als illustration zu der Erklärung in Luchian Grigore Antwort, hier ist, was die matrix-cache Präsenz sieht aus wie für die beiden Fälle von 64x64 und 65x65 Matrizen (siehe den link oben für details auf die zahlen).
Farben in der Animation unten haben folgende Bedeutung:
Den 64x64 Fall:
Bemerken, wie fast jeder Zugang zu einer neuen Zeile führt, die in einem cache-miss. Und nun, wie sieht es für den normalen Fall, ein 65x65 matrix:
Hier kannst du sehen, dass die meisten Zugriffe nach der ersten Erwärmung sind cache hits. Dies ist, wie CPU-cache soll die arbeiten im Allgemeinen.