Künstliche Intelligenz
Datenparallele Typen in C++26: SIMD-Algorithmen
Die SIMD-Bibliothek bietet in C++26 portable Typen zur expliziten Angabe von Datenparallelität und zur Strukturierung von Daten für einen effizienteren SIMD-Zugriff.
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++.
Die Kurzserie zu datenparallelen Typen hat bisher folgende Themen behandelt:
Mit dem aktuellen Beitrag schließe ich die Serie ab. Diesmal geht es um vier spezielle Algorithmen für SIMD-Vektoren: min
, max
, minmax
und clamp
.
Minimum und Maximum
Den zwei Algorithmen min
und max
ist gemein, dass sie jeweils zwei SIMD-Vektoren annehmen und einen SIMD-Vektor zurückgeben. Dieser enthält die elementweisen Minimum oder Maximum der Eingabevektoren. Der minmax
-Algorithmus nimmt ebenfalls zwei SIMD-Vektoren an und gibt ein Paar von SIMD-Vektoren zurück. Der erste Vektor des Paares enthält die elementweisen Minimum, der zweite die elementweisen Maximum der Eingabevektoren.
Das folgende Beispiel zeigt die drei Algorithmen in Aktion:
// minmax.cpp
#include
#include
#include
namespace stdx = std::experimental;
void println(auto rem, auto const v) {
std::cout << rem << ": ";
for (std::size_t i = 0; i != v.size(); ++i)
std::cout << std::setw(2) << v[i] << ' ';
std::cout << '\n';
}
void printPairs(auto rem, auto const v1) {
std::cout << rem << ": ";
for (std::size_t i = 0; i != v1.first.size(); ++i)
std::cout << '(' << v1.first[i] << ", " << v1.second[i] << ')' << ' ';
std::cout << '\n';
}
int main() {
stdx::fixed_size_simd a{[](int i) {
static constexpr auto c = {10, 9, 8, 7, 6, 5, 4, 3};
return c.begin()[i];
}};
println("a", a);
stdx::fixed_size_simd b{[](int i) {
static constexpr auto c = {3, 4, 5, 6, 7, 8, 9, 10,};
return c.begin()[i];
}};
println("b", b);
std::cout << '\n';
auto minimum = stdx::min(a, b);
println("minimum", minimum);
auto maximum = stdx::max(a, b);
println("maximum", maximum);
/*
auto minmax = stdx::minmax(a, b);
printPairs("minmax", minmax);
*/
}
Als Eingabevektoren verwende ich die SIMD-Vektoren a
und b
. Diese werden auf eine besondere Art initialisiert. Dazu lege ich eine Initialisierungsliste c
in der Lambda-Funktion an, die einen Iterator auf sie zurückgibt.
Der Screenshot zeigt die Ausgabe des Programms mit min und max.
Die Anwendung des Algorithmus minmax
habe ich auskommentiert, weil ich die Zeile stdx::minmax(a, b)
weder mit dem GCC noch mit dem clang -Compiler übersetzen konnte.
Einpassen in Grenzwerte mit clamp
std::datapar::clamp
wendet elementweise die Funktion std::clamp
auf den SIMD-Vektor an. Dabei wird jedes Element in einen minimalen und maximalen Grenzwert eingesperrt.
Das folgende Programm basiert auf einem Beispiel aus cppreference:
// clamp.cpp
#include
#include
#include
#include
#include
namespace stdx = std::experimental;
void println(auto rem, auto const v) {
std::cout << rem << ": ";
for (std::size_t i = 0; i != v.size(); ++i)
std::cout << std::setw(4) << v[i] << ' ';
std::cout << '\n';
}
int main() {
std::cout << "INT8_MIN: " << INT8_MIN << '\n';
std::cout << "INT8_MAX: " << INT8_MAX << '\n';
std::cout << "UINT8_MAX: " << UINT8_MAX << '\n';
std::cout << '\n';
stdx::fixed_size_simd a{[](int i) {
static constexpr auto c = {-129, -128, -1, 0, 42, 127, 128, 255};
return c.begin()[i];
}};
println("a", a);
stdx::fixed_size_simd lo1{INT8_MIN};
stdx::fixed_size_simd hi1{INT8_MAX};
const auto b = stdx::clamp(a, lo1, hi1);
println("b", b);
stdx::fixed_size_simd lo2{0};
stdx::fixed_size_simd hi2{UINT8_MAX};
const auto c = stdx::clamp(a, lo2, hi2);
println("c", c);
}
Der Screenshot zeigt die Ausgabe des Programms mit clamp
Schön ist in der Ausgabe des SIMD-Vektors b zu sehen, wie die Werte des SIMD-Vektors a
in die Grenzwerte INT8_MIN
und INT8_MAX
eingepasst werden. Bei dem SIMD-Vektor c
kommen hingegen die Grenzwerte 0 und UINT8_MAX
zum Einsatz.
Wie geht’s weiter?
Nun ist es Zeit für meinen zweiten Durchlauf durch den neuen C++26-Standard. Dabei werde ich mich in erster Linie auf die Funktionen konzentrieren, die ich im ersten Durchgang nicht im Detail behandelt habe.
Beginnen werde ich mit Contracts.
(rme)