Datenparallele Typen in C++26: ein Beispiel aus der Praxis
C++26 bekommt Funktionen zum Arbeiten mit datenparallelen Typen (SIMD). Ein Beispiel zeigt den praktischen Einsatz.
(Bild: SerbioVas/Shutterstock)
- Rainer Grimm
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.
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.
(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<int> SSE2-Instruktionen zum Einsatz. Diese SIMD-Vektoren sind 128 Bit groĂź.
Videos by heise
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 Elementsfixed_size: Speicherung einer bestimmten Anzahl von Elementencompatible: gewährleistet ABI-Kompatibilitätnative: am effizientestenmax_fixed_size: maximale Anzahl von Elementen, die garantiert vonfixed_sizeunterstü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)