Ich habe die komprimierte Sparse Column (csc) -Darstellung der nxn-Dreiecksmatrix A mit Nullen auf der Hauptdiagonale und möchte nach b in auflösen
(A + I)' * x = b
Dies ist die Routine, die ich habe, um dies zu berechnen:
void backsolve(const int*__restrict__ Lp,
const int*__restrict__ Li,
const double*__restrict__ Lx,
const int n,
double*__restrict__ x) {
for (int i=n-1; i>=0; --i) {
for (int j=Lp[i]; j<Lp[i+1]; ++j) {
x[i] -= Lx[j] * x[Li[j]];
}
}
}
Somit b
wird über das Argument übergeben x
und von der Lösung überschrieben. Lp
, Li
, Lx
Sind jeweils die Zeile, Indizes und die Datenzeiger in dem Standard csc Darstellung schwach besetzte Matrizen. Diese Funktion ist der oberste Hotspot im Programm mit der Zeile
x[i] -= Lx[j] * x[Li[j]];
der größte Teil der Zeit verbracht. Kompilieren mit gcc-8.3 -O3 -mfma -mavx -mavx512f
gibt
backsolve(int const*, int const*, double const*, int, double*):
lea eax, [rcx-1]
movsx r11, eax
lea r9, [r8+r11*8]
test eax, eax
js .L9
.L5:
movsx rax, DWORD PTR [rdi+r11*4]
mov r10d, DWORD PTR [rdi+4+r11*4]
cmp eax, r10d
jge .L6
vmovsd xmm0, QWORD PTR [r9]
.L7:
movsx rcx, DWORD PTR [rsi+rax*4]
vmovsd xmm1, QWORD PTR [rdx+rax*8]
add rax, 1
vfnmadd231sd xmm0, xmm1, QWORD PTR [r8+rcx*8]
vmovsd QWORD PTR [r9], xmm0
cmp r10d, eax
jg .L7
.L6:
sub r11, 1
sub r9, 8
test r11d, r11d
jns .L5
ret
.L9:
ret
Laut vtune,
vmovsd QWORD PTR [r9], xmm0
ist der langsamste Teil. Ich habe fast keine Erfahrung mit der Montage und weiß nicht, wie ich diesen Vorgang weiter diagnostizieren oder optimieren kann. Ich habe versucht, mit verschiedenen Flags zu kompilieren, um SSE, FMA usw. zu aktivieren / deaktivieren, aber nichts hat funktioniert.
Prozessor: Xeon Skylake
Frage Was kann ich tun, um diese Funktion zu optimieren?
quelle
i >= Li[j]
für allej
in der inneren Schleife?Li[j]
sindi
, Überprüfen der Annahme mit Anweisungen zur Konflikterkennung, Überprüfen, ob allei
s nicht verbunden sind, Berechnen, Streuen und Speichern der Ergebnisse. Wenn ein Konflikt festgestellt wird, greifen Sie auf die skalare Implementierung zurück.i < Li[j] < n
. Die Frage wurde aktualisiert, um die untere Dreiecksnatur von A zu erwähnen.Antworten:
Dies sollte ziemlich stark vom genauen Sparsity-Muster der Matrix und der verwendeten Plattform abhängen. Ich habe einige Dinge mit
gcc 8.3.0
und Compiler-Flags-O3 -march=native
(die sich-march=skylake
auf meiner CPU befinden) im unteren Dreieck dieser Matrix der Dimension 3006 mit Einträgen ungleich Null von 19554 getestet. Hoffentlich liegt dies etwas in der Nähe Ihres Setups, aber ich hoffe auf jeden Fall, dass diese Ihnen eine Vorstellung davon geben können, wo Sie anfangen sollen.Für das Timing habe ich Google / Benchmark mit dieser Quelldatei verwendet . Es definiert,
benchBacksolveBaseline
welche Benchmarks die in der Frage angegebene Implementierung undbenchBacksolveOptimized
welche Benchmarks die vorgeschlagenen "optimierten" Implementierungen sind. Es gibt auchbenchFillRhs
separate Benchmarks für die Funktion, die in beiden verwendet wird, um einige nicht vollständig triviale Werte für die rechte Seite zu generieren. Um die Zeit der "reinen" Backsolves zu erhalten, sollte die benötigte ZeitbenchFillRhs
abgezogen werden.1. Streng rückwärts iterieren
Die äußere Schleife in Ihrer Implementierung durchläuft die Spalten rückwärts, während die innere Schleife die aktuelle Spalte vorwärts durchläuft. Es scheint konsistenter zu sein, jede Spalte auch rückwärts zu durchlaufen:
Dies ändert kaum die Assembly ( https://godbolt.org/z/CBZAT5 ), aber die Benchmark-Timings zeigen eine messbare Verbesserung:
Ich gehe davon aus, dass dies durch einen irgendwie vorhersehbareren Cache-Zugriff verursacht wird, aber ich habe mich nicht weiter damit befasst.
2. Weniger Lasten / Speicher in der inneren Schleife
Da A ein unteres Dreieck ist, haben wir
i < Li[j]
. Daher wissen wir, dassx[Li[j]]
sich dies aufgrund der Änderungenx[i]
in der inneren Schleife nicht ändern wird. Wir können dieses Wissen mithilfe einer temporären Variablen in unsere Implementierung einbringen:Dadurch wird
gcc 8.3.0
der Speicher von innerhalb der inneren Schleife direkt nach seinem Ende in den Speicher verschoben ( https://godbolt.org/z/vM4gPD ). Der Benchmark für die Testmatrix auf meinem System zeigt eine kleine Verbesserung:3. Rollen Sie die Schleife ab
Während
clang
bereits nach der ersten vorgeschlagenen Codeänderung mit dem Abrollen der Schleife begonnen wird, ist diesgcc 8.3.0
immer noch nicht der Fall . Versuchen wir es also, indem wir zusätzlich bestehen-funroll-loops
.Beachten Sie, dass sich auch die Baseline verbessert, da die Schleife in dieser Implementierung ebenfalls abgewickelt wird. Unsere optimierte Version profitiert auch ein wenig vom Abrollen der Schleife, aber vielleicht nicht so sehr, wie es uns gefallen hat. Wenn man sich die generierte Assembly ( https://godbolt.org/z/_LJC5f ) ansieht , scheint es, als
gcc
wäre sie mit 8 Abrollvorgängen ein wenig weit gegangen. Für mein Setup kann ich mit nur einem einfachen manuellen Abrollen tatsächlich etwas besser abschneiden. Lassen Sie also die Flagge-funroll-loops
wieder fallen und implementieren Sie das Abrollen mit so etwas:Damit messe ich:
Andere Algorithmen
Alle diese Versionen verwenden immer noch dieselbe einfache Implementierung der Rückwärtslösung für die Struktur mit geringer Dichte. Inhärent kann das Arbeiten mit solchen Strukturen mit geringer Dichte erhebliche Probleme mit dem Speicherverkehr haben. Zumindest für Matrixfaktorisierungen gibt es ausgefeiltere Methoden, die mit dichten Submatrizen arbeiten, die aus der spärlichen Struktur zusammengesetzt sind. Beispiele sind übernodale und multifrontale Methoden. Ich bin ein bisschen verschwommen, aber ich denke, dass solche Methoden diese Idee auch auf das Layout anwenden und dichte Matrixoperationen für niedrigere dreieckige Rückwärtslösungen verwenden werden (zum Beispiel für Faktorisierungen vom Cholesky-Typ). Es könnte sich also lohnen, diese Art von Methoden zu untersuchen, wenn Sie nicht gezwungen sind, sich an die einfache Methode zu halten, die direkt auf der spärlichen Struktur funktioniert. Siehe zum Beispiel diese Umfrage von Davis.
quelle
-ffast-math
) Clang mehrere Akkumulatoren verwendet. GCC wird sich abrollen, aber nicht die Mühe machen, mehrere Abhängigkeitsketten zu erstellen, um die ALU-Latenz zu verbergen, wodurch der größte Teil des Zwecks für Reduktionsschleifen wie zxi_temp -=
. Wenn die Verbesserung 2. so kompiliert wird, wie wir es erwarten würden, indem die Latenz für die Speicherweiterleitung beim Laden / Neuladen vom kritischen Pfad genommen, aber um weniger als den Faktor 2 beschleunigt wird, scheint die FP-Latenz kein großer Engpass zu sein (stattdessen) Speicher- / Cache-Fehler) oder dass Dep-Ketten kurz genug sind, damit OoO Exec sie ausblenden kann.Sie können einige Zyklen rasieren, indem Sie
unsigned
anstelleint
der Indextypen verwenden, was>= 0
ohnehin sein muss:Beim Kompilieren mit dem Compiler-Explorer von Godbolt wird etwas anderer Code für den Innerloop angezeigt, wodurch die CPU-Pipeline möglicherweise besser genutzt wird. Ich kann nicht testen, aber Sie könnten es versuchen.
Hier ist der generierte Code für die innere Schleife:
quelle
unsigned
statt" wirdsize_t
die Zeichenerweiterung ohne Auswirkungen auf den Cache vermieden, und der Code unterscheidet sich geringfügig, was möglicherweise eine bessere Pipeline-Nutzung ermöglicht.unsigned
, aber ich sehe nichts, was wie ein besseres Pipelining damit aussieht. Für mich sieht es etwas schlechter aus als derint
odersize_t
Code. Wie dem auch sei, so scheint es auchgcc
versucht , Speicher zu verschwenden , indem Sieincq %rax
mitgcc-9.2.1 -march=skylake-avx512
denint
undunsigned
Fällen, in denenincl %rax
ein rex-Byte speichern würde. Ich bin heute ein bisschen unbeeindruckt von gcc.