Unsere Blogartikel

Startseite / Blogübersicht

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

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.

Modernes C++ – Rainer Grimm

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 von fixed_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.