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.
Breite von simd
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 Elementsfixed_size
: Speicherung einer bestimmten Anzahl von Elementencompatible
: gewährleistet ABI-Kompatibilitätnative
: am effizientestenmax_fixed_size
: maximale Anzahl von Elementen, die garantiert vonfixed_size
unterstützt werden
Wie geht’s weiter?
Nach diesem ersten Beispiel zu datenparallelen Typen möchte ich im nächsten Artikel genauer auf deren Funktionalität eingehen.
(rme)