
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 <experimental/simd>
durch <simd>
und den Namespace std::experimental
durch std::datapar
zu ersetzen.
#include <experimental/simd>
#include <iostream>
#include <string_view>
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<class A>
stdx::simd<int, A> my_abs(stdx::simd<int, A> x)
{
where(x < 0, x) = -x;
return x;
}
int main()
{
const stdx::native_simd<int> a = 1;
println(„a“, a);
const stdx::native_simd<int> 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<long double, 16> 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.
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<int>
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<long double, 16> 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 <cmath>
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<T>
genauer eingehen.
Breite von simd<T>
Die Breite des Datentyps native_simd<T>
wird durch die Implementierung zur Compile-Zeit bestimmt. Im Gegensatz dazu gibt der Entwickler die Breite des Datentyps fixed_size_simd<T, N>
vor.
Das Klassen-Template simd besitzt folgende Deklaration:
template< class T, class Abi = simd_abi::compatible<T> >
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<T, std::experimental::simd_abi::fixed_size<N>>;
template< class T >
using native_simd = std::experimental::simd<T, std::experimental::simd_abi::native<T>>;
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 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.