Entwicklung & Code
Datenparallele Datentypen in C++26: Reduktion eines SIMD-Vektors
Nach der Vorstellung der datenparallelen Datentypen in C++26 und einem praktischen Beispiel behandle ich in diesem Artikel die Reduktion und Maskenreduktion für datenparallele Datentypen.
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++.
Funktionen zur Reduktion
Eine Reduktion reduziert den SIMD-Vektor auf ein einzelnes Element. Die Bibliothek stellt drei Funktionen für diesen Zweck zur Verfügung: reduce
, hmin
und hmax
.
Das folgende Programm zeigt, wie diese Funktionen verwendet werden:
// reduction.cpp
#include
#include
#include
#include
#include
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';
}
int main() {
const stdx::fixed_size_simd a([](int i) { return i; });
println("a", a);
auto sum = stdx::reduce(a);
std::cout << "sum: " << sum << "\n\n";
const stdx::fixed_size_simd b([](int i) { return i + 1; });
println("b", b);
auto product = stdx::reduce(b, std::multiplies<>());
std::cout << "product: " << product << "\n\n";
auto maximum = stdx::hmax(b);
std::cout << "maximum: " << maximum << "\n\n";
auto minimum = stdx::hmin(b);
std::cout << "minimum: " << minimum << "\n\n";
}
Zunächst kommt die Funktion reduce
zum Einsatz. Standardmäßig wird der Operator + wie gewohnt angewendet. Diese Funktion kann jedoch auch mit einem beliebigen binären Operator parametrisiert werden. Der Ausdruck stdx::reduce(b, std::multiplies<>())
wendet das Funktionsobjekt std::multiplies
aus dem Header functional
an. Die Funktionen hmax
und hmin
bestimmen das Maximum und Minimum des SIMD-Vektors b
.
Die Ausgabe zeigt die Reduktion auf die Summe, das Produkt, das Minimum und das Maximum.
Maskenreduktion auf true oder false
Bei der Reduktion einer Maske wird die SIMD-Maske entweder auf den Wert true
oder auf den Wert false
reduziert.
Hier begegnen wir einigen alten Bekannten aus C++: all_of
, any_of
, none_of
und some_of
.
all_of
: Gibttrue
zurück, wenn alle Werte in der SIMD-Masketrue
any_of
: Gibttrue
zurück, wenn mindestens ein Wert in der SIMD-Masketrue
none_of
: Gibttrue
zurück, wenn alle Werte in der SIMD-Maskefalse
some_of
: Gibttrue
zurück, wenn mindestens ein Wert in der SIMD-Masketrue
ist, aber nicht alle Werte darintrue
cppreference.com enthält ein schönes Beispiel für diese Funktionen:
// reductionWithMask.cpp
#include
#include
namespace stq = std::experimental;
int main()
{
using mask = stq::fixed_size_simd_mask;
mask mask1{false}; // = {0, 0, 0, 0}
assert
(
stq::none_of(mask1) == true &&
stq::any_of(mask1) == false &&
stq::some_of(mask1) == false &&
stq::all_of(mask1) == false
);
mask mask2{true}; // = {1, 1, 1, 1}
assert
(
stq::none_of(mask2) == false &&
stq::any_of(mask2) == true &&
stq::some_of(mask2) == false &&
stq::all_of(mask2) == true
);
mask mask3{true};
mask3[0] = mask3[1] = false; // mask3 = {0, 0, 1, 1}
assert
(
stq::none_of(mask3) == false &&
stq::any_of(mask3) == true &&
stq::some_of(mask3) == true &&
stq::all_of(mask3) == false
);
}
popcount
bestimmt, wie viele Werte in einer SIMD-Maske true
sind. Ein Programm, das dies ausführt, lässt sich schnell schreiben:
// popcount.cpp
#include
#include
#include
namespace stdx = std::experimental;
void println(std::string_view name, auto const& a) {
std::cout << std::boolalpha << name << ": ";
for (std::size_t i{}; i != std::size(a); ++i)
std::cout << a[i] << ' ';
std::cout << '\n';
}
int main() {
const stdx::native_simd a = 1;
println("a", a);
const stdx::native_simd b([](int i) { return i - 2; });
println("b", b);
const auto c = a + b;
println("c", c);
const stdx::native_simd_mask x = c < 0;
println("x", x);
auto cnt = popcount(x);
std::cout << "cnt: " << cnt << '\n';
}
Die Programmausgabe des popcount-Beispiels ist selbsterklärend.
Darüber hinaus gibt es zwei weitere weitere Funktionen:
find_first_set
: Gibt den niedrigsten Indexi
zurück, bei dem die SIMD-Masketrue
ist.find_last_set
: Gibt den größten Indexi
zurück, bei dem die SIMD-Masketrue
ist.
Das folgende Programm demonstriert die Verwendung der beiden Funktionen:
// find_first_set.cpp
#include
#include
#include
namespace stdx = std::experimental;
void println(std::string_view name, auto const& a) {
std::cout << std::boolalpha << name << ": ";
for (std::size_t i{}; i != std::size(a); ++i)
std::cout << a[i] << ' ';
std::cout << '\n';
}
int main() {
stdx::simd_mask x{0};
println("x", x);
x[1] = true;
x[x.size() - 1] = true;
println("x", x);
auto first = stdx::find_first_set(x);
std::cout << "find_first_set(x): " << first << '\n';
auto last = stdx::find_last_set(x);
std::cout << "find_last_set(x): " << last<< '\n';
}
Die Ausgabe zeigt die Suche nach dem ersten und letzten true im Vektor.
Wie geht‘s weiter?
In meinem nächsten Artikel werde ich mich auf die Algorithmen datenparalleler Datentypen konzentrieren.
(rme)