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.

vorlesen Druckansicht 13 Kommentare lesen
HolzwĂĽrfel mit dem Schriftzug C++

(Bild: SerbioVas/Shutterstock)

Lesezeit: 4 Min.
Von
  • 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.

Modernes C++ – Rainer Grimm
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.

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

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

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

(rme)