Entwicklung & Code

Datenparallele Typen in C++26: ein Beispiel aus der Praxis


Nachdem ich in meinem letzten Artikel Neuerungen in C++26: Datenparallele Typen (SIMD) eine theoretische Einführung in das neue Feature von C++ 26 gegeben habe, möchte ich heute mit einem praktischen Beispiel fortfahren.




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++.

Das folgende Einführungsbeispiel stammt aus der experimentellen Implementierung der SIMD-Bibliothek. Diese Funktionalität wurde unter dem Namen „Data-parallel types (SIMD)“ vollständig in den Entwurf für C++ 26 übernommen. Um das Programm auf den C++ 26-Standard zu portieren, sollte es ausreichen, den Header durch und den Namespace std::experimental durch std::datapar zu ersetzen.


#include 
#include 
#include 
namespace stdx = std::experimental;
 
void println(std::string_view name, auto const& a)
{
    std::cout << name << „: “;
    for (std::size_t i{}; i != std::size(a); ++i)
        
std::cout << a[i] << ‚ ‘;
    std::cout << ‚\n‘;
}
 
template
stdx::simd my_abs(stdx::simd x)
{
    where(x < 0, x) = -x;
    return x;
}
 
int main()
{
    const stdx::native_simd a = 1;
    println(„a“, a);
 
    const stdx::native_simd b([](int i) { return i - 2; });
    println(„b“, b);
     
const auto c = a + b;
    println(„c“, c);
 
const auto d = my_abs(c);
println(‚d‘, d);
 
const auto e = d * d;
println(„e“, e);
     
const auto inner_product = stdx::reduce(e);
    std::cout << „inner product: “ << inner_product << ‚\n‘;
 
const stdx::fixed_size_simd x([](int i) { return i; });
println(„x“, x);
    
println(„cos²(x) + sin²(x)“, stdx::pow(stdx::cos(x), 2) + stdx::pow(stdx::sin(x), 2));
}


Bevor ich mit dem Programm fortfahre, möchte ich die Ausgabe vorstellen.



Ausgabe des Beispielcodes

(Bild: Screenshot (Rainer Grimm))

Zuerst möchte ich mich den Funktionen println und my_abs widmen. println gibt den Namen und den Inhalt eines SIMD-Vektors aus und durchläuft dabei dessen Elemente. my_abs berechnet den Absolutwert jedes Elements in einem SIMD-Vektor mit Ganzzahlen und verwendet dabei where, um negative Werte bedingt zu negieren. Deutlich interessanter ist die main-Funktion.

Bei dem SIMD-Vektor a wird jedes Element auf 1 gesetzt, hingegen wird bei dem SIMD-Vektor b dank der Lambda-Funktion jedes Element so initialisiert, dass dieses seinen Index minus 2 besitzt. Dabei kommen per Default durch const stdx::native_simd SSE2-Instruktionen zum Einsatz. Diese SIMD-Vektoren sind 128 Bit groß.

Nun beginnt die Arithmetik. Vektor c ist die elementweise Summe von a und b, d ist der elementweise absolute Wert von c und der Vektor e ist das elementweise Quadrat von d. Zuletzt kommt stdx::reduce(e) zum Einsatz. Dabei wird der Vektor e auf seine Summe reduziert.

Besonders interessant ist der Ausdruck const stdx::fixed_size_simd x([](int i) { return i; }). Durch ihn wird der SIMD-Vektor x mit 16 long-double-Werten von 0 bis 15 initialisiert. Das ist möglich, wenn die Hardware hinreichend modern ist und AVX-252 unterstützt, beispielsweise mit Intels Xeon-Phi- oder AMDs Zen-4-Architektur.

Ähnlich interessant ist die Zeile println("cos²(x) + sin²(x)", stdx::pow(stdx::cos(x), 2) + stdx::pow(stdx::sin(x), 2)). Sie berechnet cos²(x) + sin²(x) für jedes Element, was aufgrund der trigonometrischen Identität des Pythagoras für alle Elemente 1 ist. Es gilt, dass alle Funktionen in außer den speziellen mathematischen Funktionen für simd überladen sind. Dies sind zum Beispiel die grundlegenden Funktionen wie abs, min oder max. Aber auch zum Beispiel exponentielle, trigonometrische, hyperbolische, Potenz- oder Gamma-Funktionen lassen sich direkt auf SIMD Vektor anwenden.

Nun möchte ich noch auf die Breite des Datentyps simd genauer eingehen.

Die Breite des Datentyps native_simd wird durch die Implementierung zur Compile-Zeit bestimmt. Im Gegensatz dazu gibt der Entwickler die Breite des Datentyps fixed_size_simd vor.

Das Klassen-Template simd besitzt folgende Deklaration:


template< class T, class Abi = simd_abi::compatible >
class simd;


Dabei steht T für den Elementtyp, der nicht bool sein kann. Durch den Abi-Tag wird die Anzahl der Elemente und deren Speicher bestimmt.

Zu diesem Klassen-Template gibt es zwei Aliase:


template< class T, int N >
using fixed_size_simd = std::experimental::simd>;
template< class T >
using native_simd = std::experimental::simd>;


Folgende ABI-Tags stehen zur Verfügung:

  • scalar: Speicherung eines einzelnen Elements
  • fixed_size: Speicherung einer bestimmten Anzahl von Elementen
  • compatible: gewährleistet ABI-Kompatibilität
  • native: am effizientesten
  • max_fixed_size: maximale Anzahl von Elementen, die garantiert von fixed_size unterstützt werden

Nach diesem ersten Beispiel zu datenparallelen Typen möchte ich im nächsten Artikel genauer auf deren Funktionalität eingehen.


(rme)



Source link

Leave a Reply

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

Beliebt

Die mobile Version verlassen