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 (ab+ca \cdot b + c).
    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. mit i < (result.size() / 4) * 4 oder i < result.size() & ~static_cast<std::size_t>(0b11) oder i < 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 gar vector::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 den std::vector verwenden, der Speicher mit dem passenden Alignment reserviert, oder ein C-array/ std::array
    fester Größe mit alignas(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.


Anmelden zum Antworten