Ich prügele mich immer mit dem Kopf über den Kopf. Ich habe einen SSE-basierten Algorithmus zum Multiplizieren der Matrix A
mit der Matrix B
. Ich muss auch die Operationen implementieren, für die A, B oder beide transponiert sind. Ich habe eine naive Implementierung davon gemacht, den 4x4-Matrix-Code, der unten dargestellt ist (was ziemlich normale SSE-Operationen sind, glaube ich), aber die Operation A*B^T
dauert ungefähr doppelt so lang wie A*B
. Die ATLAS-Implementierung gibt ähnliche Werte für A*B
zurück, und fast identische Ergebnisse für die Multiplikation mit einer Transponierung, was für mich einen effizienten Weg zu dieser Vorgehensweise nahelegt.Wie kann ich Transpositionsmatrizen effizienter multiplizieren?
MM-Multiplikation:
m1 = (mat1.m_>>2)<<2;
n2 = (mat2.n_>>2)<<2;
n = (mat1.n_>>2)<<2;
for (k=0; k<n; k+=4) {
for (i=0; i<m1; i+=4) {
// fetch: get 4x4 matrix from mat1
// row-major storage, so get 4 rows
Float* a0 = mat1.el_[i]+k;
Float* a1 = mat1.el_[i+1]+k;
Float* a2 = mat1.el_[i+2]+k;
Float* a3 = mat1.el_[i+3]+k;
for (j=0; j<n2; j+=4) {
// fetch: get 4x4 matrix from mat2
// row-major storage, so get 4 rows
Float* b0 = mat2.el_[k]+j;
Float* b1 = mat2.el_[k+1]+j;
Float* b2 = mat2.el_[k+2]+j;
Float* b3 = mat2.el_[k+3]+j;
__m128 b0r = _mm_loadu_ps(b0);
__m128 b1r = _mm_loadu_ps(b1);
__m128 b2r = _mm_loadu_ps(b2);
__m128 b3r = _mm_loadu_ps(b3);
{ // first row of result += first row of mat1 * 4x4 of mat2
__m128 cX1 = _mm_add_ps(_mm_mul_ps(_mm_load_ps1(a0+0), b0r), _mm_mul_ps(_mm_load_ps1(a0+1), b1r));
__m128 cX2 = _mm_add_ps(_mm_mul_ps(_mm_load_ps1(a0+2), b2r), _mm_mul_ps(_mm_load_ps1(a0+3), b3r));
Float* c0 = this->el_[i]+j;
_mm_storeu_ps(c0, _mm_add_ps(_mm_add_ps(cX1, cX2), _mm_loadu_ps(c0)));
}
{ // second row of result += second row of mat1 * 4x4 of mat2
__m128 cX1 = _mm_add_ps(_mm_mul_ps(_mm_load_ps1(a1+0), b0r), _mm_mul_ps(_mm_load_ps1(a1+1), b1r));
__m128 cX2 = _mm_add_ps(_mm_mul_ps(_mm_load_ps1(a1+2), b2r), _mm_mul_ps(_mm_load_ps1(a1+3), b3r));
Float* c1 = this->el_[i+1]+j;
_mm_storeu_ps(c1, _mm_add_ps(_mm_add_ps(cX1, cX2), _mm_loadu_ps(c1)));
}
{ // third row of result += third row of mat1 * 4x4 of mat2
__m128 cX1 = _mm_add_ps(_mm_mul_ps(_mm_load_ps1(a2+0), b0r), _mm_mul_ps(_mm_load_ps1(a2+1), b1r));
__m128 cX2 = _mm_add_ps(_mm_mul_ps(_mm_load_ps1(a2+2), b2r), _mm_mul_ps(_mm_load_ps1(a2+3), b3r));
Float* c2 = this->el_[i+2]+j;
_mm_storeu_ps(c2, _mm_add_ps(_mm_add_ps(cX1, cX2), _mm_loadu_ps(c2)));
}
{ // fourth row of result += fourth row of mat1 * 4x4 of mat2
__m128 cX1 = _mm_add_ps(_mm_mul_ps(_mm_load_ps1(a3+0), b0r), _mm_mul_ps(_mm_load_ps1(a3+1), b1r));
__m128 cX2 = _mm_add_ps(_mm_mul_ps(_mm_load_ps1(a3+2), b2r), _mm_mul_ps(_mm_load_ps1(a3+3), b3r));
Float* c3 = this->el_[i+3]+j;
_mm_storeu_ps(c3, _mm_add_ps(_mm_add_ps(cX1, cX2), _mm_loadu_ps(c3)));
}
}
// Code omitted to handle remaining rows and columns
}
für das MT Multiplikation (Matrix durch transponierten Matrix multipliziert), anstelle I B0R gespeichert mit den folgenden Befehlen B3R und verändert die Schleifenvariablen entsprechend:
__m128 b0r = _mm_set_ps(b3[0], b2[0], b1[0], b0[0]);
__m128 b1r = _mm_set_ps(b3[1], b2[1], b1[1], b0[1]);
__m128 b2r = _mm_set_ps(b3[2], b2[2], b1[2], b0[2]);
__m128 b3r = _mm_set_ps(b3[3], b2[3], b1[3], b0[3]);
Ich vermute, dass die Verlangsamung teilweise auf den Unterschied zwischen dem Ziehen einer Reihe zu einer Zeit und dem Speichern von 4 Werten jedes Mal zurückzuführen ist, um die Spalte zu erhalten, aber ich fühle mich wie der andere Weg, um in Reihen von B zu ziehen und dann mit dem co multiplizieren lumn of As, verschiebt die Kosten einfach auf 4 Spalten mit Ergebnissen.
Ich habe auch versucht, B-Zeilen als Zeilen ziehen und dann _MM_TRANSPOSE4_PS(b0r, b1r, b2r, b3r);
verwenden, um die Transposition (ich dachte, es könnte einige zusätzliche Optimierungen in diesem Makro sein), aber es gibt keine echte Verbesserung.
Auf der Oberfläche, ich fühle mich wie dies schneller sein sollte ... die Punktprodukte eine Reihe durch eine Reihe wäre beteiligt, die von Natur aus effizienten scheint, aber versucht, die Punktprodukte gerade nach oben gerade ergibt, die zu tun Machen Sie dasselbe, um die Ergebnisse zu speichern.
Was fehlt mir hier?
Hinzugefügt: Nur um zu verdeutlichen, versuche ich nicht die Matrizen zu transponieren. Ich würde es vorziehen, entlang ihnen zu iterieren. Das Problem, so gut ich es beurteilen kann, ist, dass der Befehl _mm_set_ps viel langsamer ist als _mm_load_ps.
Ich habe auch eine Variation, wo ich die 4 Zeilen der Matrix A gespeichert und dann ersetzt die 4 Curly-klammert Segmente, die 1 Last, 4 Multiplikationen und 2 addiert mit 4 Multiplikationsbefehle und 3 hadds
, aber mit wenig Erfolg . Der Zeitpunkt bleibt gleich (und ja, ich versuchte es mit einer Debug-Anweisung, um sicherzustellen, dass der Code in meinem Test-Kompilierung Said Debug-Anweisung vor dem Profilieren entfernt wurde geändert hatte, natürlich.):
{ // first row of result += first row of mat1 * 4x4 of mat2
__m128 cX1 = _mm_hadd_ps(_mm_mul_ps(a0r, b0r), _mm_mul_ps(a0r, b1r));
__m128 cX2 = _mm_hadd_ps(_mm_mul_ps(a0r, b2r), _mm_mul_ps(a0r, b3r));
Float* c0 = this->el_[i]+j;
_mm_storeu_ps(c0, _mm_add_ps(_mm_hadd_ps(cX1, cX2), _mm_loadu_ps(c0)));
}
{ // second row of result += second row of mat1 * 4x4 of mat2
__m128 cX1 = _mm_hadd_ps(_mm_mul_ps(a1r, b0r), _mm_mul_ps(a1r, b1r));
__m128 cX2 = _mm_hadd_ps(_mm_mul_ps(a1r, b2r), _mm_mul_ps(a1r, b3r));
Float* c0 = this->el_[i+1]+j;
_mm_storeu_ps(c0, _mm_add_ps(_mm_hadd_ps(cX1, cX2), _mm_loadu_ps(c0)));
}
{ // third row of result += third row of mat1 * 4x4 of mat2
__m128 cX1 = _mm_hadd_ps(_mm_mul_ps(a2r, b0r), _mm_mul_ps(a2r, b1r));
__m128 cX2 = _mm_hadd_ps(_mm_mul_ps(a2r, b2r), _mm_mul_ps(a2r, b3r));
Float* c0 = this->el_[i+2]+j;
_mm_storeu_ps(c0, _mm_add_ps(_mm_hadd_ps(cX1, cX2), _mm_loadu_ps(c0)));
}
{ // fourth row of result += fourth row of mat1 * 4x4 of mat2
__m128 cX1 = _mm_hadd_ps(_mm_mul_ps(a3r, b0r), _mm_mul_ps(a3r, b1r));
__m128 cX2 = _mm_hadd_ps(_mm_mul_ps(a3r, b2r), _mm_mul_ps(a3r, b3r));
Float* c0 = this->el_[i+3]+j;
_mm_storeu_ps(c0, _mm_add_ps(_mm_hadd_ps(cX1, cX2), _mm_loadu_ps(c0)));
}
Update: Richtig, und das Laden der Zeilen von a0r
zu a3r
in die geschweiften Klammern in einem Versuch, Register Thrashing zu vermeiden fehlgeschlagen.
Ist Ihnen klar, dass Sie die Matrix nicht wirklich transponieren müssen, sondern nur auf eine andere Weise darauf zugreifen? – olivecoder
Es sieht so aus, als ob Sie tatsächlich Ihre Matrix transponieren? Das wäre natürlich langsamer. – RandyGaul
Eigentlich versuche ich es an Ort und Stelle zu tun, indem ich auf die Elemente in einer anderen Reihenfolge zugreife. Ich habe es mit einer In-Place-Transponierung versucht und ungefähr das gleiche Timing bekommen. –