Entwicklung & Code

Neuerungen in C++26: Datenparallele Datentypen (SIMD)


Die SIMD-Bibliothek bietet in C++26 portable Typen zur expliziten Angabe von Datenparallelität und zur Strukturierung von Daten für einen effizienteren SIMD-Zugriff. Bevor wir uns im Detail mit der neuen Bibliothek befassen, möchte ich kurz ein paar allgemeine Anmerkungen zu SIMD (Single Instruction, Multiple Data) voranschicken.




Rainer Grimm ist seit vielen Jahren als Softwarearchitekt, Team- und Schulungsleiter tätig. Er schreibt gerne Artikel zu den Programmiersprachen C++, Python und Haskell, spricht aber auch gerne und häufig auf Fachkonferenzen. Auf seinem Blog Modernes C++ beschäftigt er sich intensiv mit seiner Leidenschaft C++.

Vektorisierung bezieht sich auf die SIMD-Erweiterungen (Single Instruction, Multiple Data) des Befehlssatzes moderner Prozessoren. SIMD ermöglicht es dem Prozessor, eine Operation parallel auf mehrere Daten anzuwenden.

Ein einfaches Beispiel: Ob ein Algorithmus parallel und vektorisiert ausgeführt wird, hängt von vielen Faktoren ab – unter anderem davon, ob die CPU und das Betriebssystem SIMD-Befehle unterstützen. Außerdem kommt es auf den Compiler und den Optimierungsgrad an, der zum Kompilieren des Codes eingesetzt wird.


// SIMD.cpp

const int SIZE= 8;

int vec[]={1,2,3,4,5,6,7,8};
int res[SIZE]={0,};

int main(){
  for (int i= 0; i < SIZE; ++i) {
    res[i]= vec[i]+5; // (1)
  }
}


Zeile 1 ist die Schlüsselzeile in dem kleinen Programm. Dank des Compiler Explorers ist es recht einfach, die Assemblerbefehle für Clang 3.6 mit und ohne maximale Optimierung (-O3) zu generieren.

Obwohl meine Zeit, in der ich mit Assemblerbefehlen herumgespielt habe, lange vorbei ist, ist es offensichtlich, dass alles sequenziell ausgeführt wird:



(Bild: Rainer Grimm)

Durch die Verwendung der maximalen Optimierung erhalte ich Befehle, die parallel auf mehreren Datensätzen ausgeführt werden:



(Bild: Rainer Grimm)

Die Move-Operation (movdqa) und die Add-Operation (paddd) verwenden die speziellen Register xmm0 und xmm1. Beide Register sind sogenannte SSE-Register mit einer Breite von 128 Bit. Damit können 4 ints auf einmal verarbeitet werden. SSE steht für Streaming SIMD Extensions. Leider sind Vektorbefehle stark von der eingesetzten Architektur abhängig. Weder die Befehle noch die Registerbreiten sind einheitlich.

Moderne Intel-Architekturen unterstützen meist AVX2 oder sogar AVX-512. Dies ermöglicht 256-Bit- oder 512-Bit-Operationen. Damit können 8 oder 16 ints parallel verarbeitet werden. AVX steht für Advanced Vector Extension.

Genau hier kommen die neuen datenparallelen Datentypen der Bibliothek ins Spiel, die eine einheitliche Schnittstelle zu Vektorbefehlen bieten.

Bevor ich mich mit der neuen Bibliothek beschäftige, sind einige Definitionen erforderlich. Diese Definitionen beziehen sich auf den Proposal P1928R15. Insgesamt umfasst die neue Bibliothek sechs Proposals.



(Bild: Rainer Grimm)

Die Menge der vektorisierbaren Typen umfasst alle Standard-Ganzzahltypen, Zeichentypen sowie die Typen float und double. Darüber hinaus sind std::float16_t, std::float32_t und std::float64_t vektorisierbare Typen, sofern sie definiert sind.

Der Begriff datenparallel bezieht sich auf alle aktivierten Spezialisierungen der Klassen-Templates basic_simd und basic_simd_mask. Ein datenparalleles Objekt ist ein Objekt vom datenparallelen Typ.

Ein datenparalleler Typ besteht aus einem oder mehreren Elementen eines zugrunde liegenden vektorisierbaren Typs, der als Elementtyp bezeichnet wird. Die Anzahl der Elemente ist für jeden datenparallelen Typ eine Konstante und wird als Breite dieses Typs bezeichnet. Die Elemente in einem datenparallelen Typ werden von 0 bis Breite −1 indiziert.

Eine elementweise Operation wendet eine bestimmte Operation auf die Elemente eines oder mehrerer datenparalleler Objekte an. Jede solche Anwendung ist in Bezug auf die anderen nicht sequenziell. Eine unäre elementweise Operation ist eine elementweise Operation, die eine unäre Operation auf jedes Element eines datenparallelen Objekts anwendet. Eine binäre elementweise Operation ist eine elementweise Operation, die eine binäre Operation auf entsprechende Elemente zweier datenparallelisierter Objekte anwendet.

Nach so viel Theorie möchte ich nun ein kleines Beispiel zeigen. Es stammt von Matthias Kretz, Autor des Proposals P1928R15. Das Beispiel aus seiner Präsentation auf der CppCon 2023 zeigt eine Funktion f, die einen Vektor entgegennimmt und dessen Elemente auf ihre Sinuswerte abbildet:


 void f(std::vector& data) {
    using floatv = std::simd;
    for (auto it = data.begin(); it < data.end(); it += floatv::size()) {
        floatv v(it);
        v = std::sin(v);
        v.copy_to(it);
    }
}


Die Funktion f nimmt einen Vektor von Floats (data) als Referenz. Sie definiert floatv als SIMD-Vektor von Floats unter Verwendung von std::simd. f durchläuft den Vektor in Blöcken, wobei jeder Block die Größe des SIMD-Vektors hat.

Für jeden Block gilt:

  • Lädt den Block in einen SIMD-Vektor (floatv v(it);).
  • Wendet die Sinusfunktion gleichzeitig auf alle Elemente im SIMD-Vektor an (v = std::sin(v);).
  • Schreibt die Ergebnisse zurück in den ursprünglichen Vektor (v.copy_to(it);).

Die Behandlung von SIMD-Anweisungen wird besonders elegant, wenn der Proposal P0350R4 in C++26 implementiert wird. SIMD kann dann beispielsweise als neue Execution Policy in Algorithmen verwendet werden:


void f(std::vector& data) {
    std::for_each(std::execution::simd, data.begin(), data.end(), [](auto& v) {
        v = std::sin(v);
    });
}


In meinem nächsten Artikel werde ich mich näher mit der neuen SIMD-Bibliothek befassen.


(map)



Source link

Leave a Reply

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert

Beliebt

Die mobile Version verlassen