AVX2.0 Register effizient füllen
-
Hallo zusammen,
ich steh momentan leider gerade auf dem Schlauch.
Ich habe folgendes Szenario:
Ich möchte zwei Vektoren der gleichen Länge elementweise miteinander addieren und dafür AVX Register nutzen. Mit AVX2.0 passen 4 Double in ein Register. Wenn die Länge meiner Vektoren ein Vielfaches von 4 ist, ist das ja relative simpel. Wenn die Länge jedoch nicht einem Vielfachen von 4 entspricht, stehe ich irgendwie auf dem Schlauch. Wie verhindere ich dann, dass mein Index in der Schleife out of bound läuft? Und wie fülle ich dann die "leeren" Stellen im Register mit Nullen?
Anbei ein Stück Code für Vektoren deren Länge ein Vielfaches von 4 ist.for (int i = 0; i < (result.size() + (result.size() % 4)); i += 4) { __m256d AVXx1_k = _mm256_set_pd(x1_k.at(i + 3), x1_k.at(i + 2), x1_k.at(i + 1), x1_k.at(i)); __m256d AVXx2_k = _mm256_set_pd(x2_k.at(i + 3), x2_k.at(i + 2), x2_k.at(i + 1), x2_k.at(i)); __m256d AVXparameter_k = _mm256_set_pd(parameter_k, parameter_k, parameter_k, parameter_k); __m256d AVXresult = _mm256_fmadd_pd(AVXx2_k, AVXparameter_k, AVXx1_k); result.at(i) = AVXresult.m256d_f64[0]; result.at(i + 1) = AVXresult.m256d_f64[1]; result.at(i + 2) = AVXresult.m256d_f64[2]; result.at(i + 3) = AVXresult.m256d_f64[3]; }
Vielen Dank für eure Hile im Voraus.
-
Desert Storm schrieb:
Ich möchte zwei Vektoren der gleichen Länge elementweise miteinander addieren
Nur addieren oder auch multiplizieren? Weil
_mm256_fmadd_pd
ist eine fused multiply–add-Operation ().
Möglicherweise willst du hier eher_mm256_add_pd
verwenden (?).Desert Storm schrieb:
Wenn die Länge meiner Vektoren ein Vielfaches von 4 ist, ist das ja relative simpel. Wenn die Länge jedoch nicht einem Vielfachen von 4 entspricht, stehe ich irgendwie auf dem Schlauch. Wie verhindere ich dann, dass mein Index in der Schleife out of bound läuft?
Indem du in der Schleifeneintrittsbedingung nur bis zum größten Vielfachen von 4 läufst, das kleiner oder gleich
result.size()
ist.
z.B. miti < (result.size() / 4) * 4
oderi < result.size() & ~static_cast<std::size_t>(0b11)
oderi < result.size() - result.size() % 4
,
die sind alle äquivalent. Möglicherweise hast du letztere Variante auch beabsichtigt, aber versehentlich ein+
verwendet.
Natürlich musst du nach der Schleife noch die restlichen Elemente behandeln...Desert Storm schrieb:
Und wie fülle ich dann die "leeren" Stellen im Register mit Nullen?
Du setzt doch hier schon die einzelnen Elemente des Vektorregisters "manuell":
__m256d AVXx1_k = _mm256_set_pd(x1_k.at(i + 3), x1_k.at(i + 2), x1_k.at(i + 1), x1_k.at(i));
Warum also nicht genauso?:
int r = result.size() % 4; if (r > 0) { __m256d AVXx1_k = _mm256_set_pd(0, r >= 3 ? x1_k.at(i + 2) : 0, r >= 2 ? x1_k.at(i + 1) : 0, x1_k.at(i)); ...
Andere Möglichkeit: Dafür sorgen, dass die Größe der
std::vector
immer eine durch 4 teilbare Größe hat und die überschüssigen
Elemente einfach ignorieren. Das sollte den Code vereinfachen und sich nicht sonderlich auf die Performance auswirken, da
Speicherzugriffe ohnehin immer in Cache-Line-Häppchen erfolgen (64 Bytes auf x86, da passen alle SIMD-Register sauber rein).Ferner: Falls du tatsächlich Wert auf Geschwindigkeit legst, solltest du die Vektorregister-Elemente vielleicht nicht einzeln setzen
oder garvector::at
verwenden (bounds check + exception), sondern die Werte direkt aus dem Speicher laden:__m256d AVXx1_k = _mm256_load_pd(&x1_k[i]);
Hierzu ist es allerdings zwingend erforderlich, dass die Elemente des
std::vector
32-Byte-aligned sind. D.h. entweder einen
Allocator für denstd::vector
verwenden, der Speicher mit dem passenden Alignment reserviert, oder ein C-array/std::array
fester Größe mitalignas(32)
. Alternativ kannst du auch deine AVX-Addition so anpassen, dass sie erst ab dem ersten passended
"alignten" Element mit_mm256_load_pd
loslegt, also ab dem Index, wo die untersten 5 Bits der Elementadresse alle 0 sind:
reinterpret_cast<std::uintptr_t>(&x1_k[i]) & 0b11111 == 0
Das wird also schnell etwas komplizierter, lässt sich aber nicht vermeiden, wenn man SIMD wirklich effizient nutzen will.Ansonsten: Falls es dir nur um solche simplen Sachen und nicht um komplexere Berechnungen geht, fährst du wahrscheinlich
schmerzfreier, wenn du auf die Vektorisierungs-Optimierungen deines Compilers vertaust. Deine Additionsschleife sollte der noch
recht gut selbständig hinbekommen: z.B. mit den Flags/arch:AVX2
für den MS-Compiler, oder-mavx2
für GCC/Clang, zuzüglich
hohem Optimierungslevel bzw. entsprechenden Flags wie bspw.-ftree-vectorize
.Gruss,
Finnegan
-
Vielen herzlichen Dank für die extrem informative und hilfreiche Antwort. Ich denke, dass das meine Probleme lösen wird. Auch die Hinweise zur Performance nehme ich gerne an:) Ich werde die Punkte mal versuchen Schritt für Schritt umzusetzen.
Bzgl. deiner ersten Frage: Es war schon beabsichtigt, dass ich eine addieren und multiplizieren möchte. Trotzdem Danke für den Hinweis.
Viele Grüße.