Highway ist eine C++-Bibliothek, die portable SIMD/Vektor-Intrinsics bereitstellt.
Dokumentation
Zuvor unter Apache 2 lizenziert, jetzt doppelt lizenziert als Apache 2 / BSD-3.
Wir brennen für leistungsstarke Software. Großes ungenutztes Potenzial sehen wir bei CPUs (Server, Mobilgeräte, Desktops). Highway richtet sich an Ingenieure, die zuverlässig und wirtschaftlich die Grenzen des Software-Möglichen erweitern möchten.
CPUs stellen SIMD-/Vektoranweisungen bereit, die denselben Vorgang auf mehrere Datenelemente anwenden. Dadurch kann der Energieverbrauch beispielsweise um das Fünffache gesenkt werden, da weniger Anweisungen ausgeführt werden. Wir sehen auch oft 5- bis 10-fache Beschleunigungen.
Highway macht die SIMD-/Vektorprogrammierung nach diesen Leitprinzipien praktisch und praktikabel:
Erfüllt, was Sie erwarten : Highway ist eine C++-Bibliothek mit sorgfältig ausgewählten Funktionen, die ohne umfangreiche Compiler-Transformationen gut auf CPU-Anweisungen abgebildet werden können. Der resultierende Code ist vorhersehbarer und robuster gegenüber Codeänderungen/Compiler-Updates als die Autovektorisierung.
Funktioniert auf weit verbreiteten Plattformen : Highway unterstützt fünf Architekturen; Derselbe Anwendungscode kann auf verschiedene Befehlssätze abzielen, einschließlich solcher mit „skalierbaren“ Vektoren (Größe zur Kompilierzeit unbekannt). Highway erfordert nur C++11 und unterstützt vier Compilerfamilien. Wenn Sie Highway auf anderen Plattformen nutzen möchten, melden Sie bitte ein Problem.
Flexibel bereitzustellen : Anwendungen, die Highway verwenden, können auf heterogenen Clouds oder Client-Geräten ausgeführt werden, wobei zur Laufzeit der beste verfügbare Befehlssatz ausgewählt wird. Alternativ können sich Entwickler dafür entscheiden, einen einzelnen Befehlssatz ohne Laufzeitaufwand zu verwenden. In beiden Fällen ist der Anwendungscode derselbe, mit Ausnahme des Austauschs HWY_STATIC_DISPATCH
durch HWY_DYNAMIC_DISPATCH
plus einer Codezeile. Siehe auch @kfjahnkes Einführung zum Dispatching.
Geeignet für eine Vielzahl von Domänen : Highway bietet einen umfangreichen Satz an Operationen, die für die Bildverarbeitung (Gleitkomma), Komprimierung, Videoanalyse, lineare Algebra, Kryptographie, Sortierung und Zufallsgenerierung verwendet werden. Wir sind uns bewusst, dass für neue Anwendungsfälle möglicherweise zusätzliche Operationen erforderlich sind, und fügen diese gerne dort hinzu, wo es sinnvoll ist (z. B. keine Leistungseinbußen auf einigen Architekturen). Wenn Sie darüber diskutieren möchten, reichen Sie bitte ein Problem ein.
Belohnt datenparalleles Design : Highway bietet Tools wie Gather, MaskedLoad und FixedTag, um Beschleunigungen für ältere Datenstrukturen zu ermöglichen. Die größten Vorteile lassen sich jedoch durch die Entwicklung von Algorithmen und Datenstrukturen für skalierbare Vektoren erzielen. Zu den hilfreichen Techniken gehören Stapelverarbeitung, Array-Struktur-Layouts und ausgerichtete/aufgefüllte Zuweisungen.
Wir empfehlen diese Ressourcen für den Einstieg:
Online-Demos mit Compiler Explorer:
Wir stellen fest, dass Highway in den folgenden Open-Source-Projekten erwähnt wird, die über sourcegraph.com gefunden werden. Bei den meisten handelt es sich um GitHub-Repositories. Wenn Sie Ihr Projekt hinzufügen oder direkt darauf verlinken möchten, können Sie gerne ein Problem ansprechen oder uns über die untenstehende E-Mail kontaktieren.
Andere
Wenn Sie Highway erhalten möchten, können Sie es nicht nur aus diesem GitHub-Repository klonen oder es als Git-Submodul verwenden, sondern es auch in den folgenden Paketmanagern oder Repositorys finden:
Siehe auch die Liste unter https://repology.org/project/highway-simd-library/versions.
Highway unterstützt 24 Ziele, aufgelistet in alphabetischer Reihenfolge der Plattform:
EMU128
, SCALAR
;NEON_WITHOUT_AES
, NEON
, NEON_BF16
, SVE
, SVE2
, SVE_256
, SVE2_128
;Z14
, Z15
;PPC8
(v2.07), PPC9
(v3.0), PPC10
(v3.1B, aufgrund von Compilerfehlern noch nicht unterstützt, siehe #1207; erfordert auch QEMU 7.2);RVV
(1,0);WASM
, WASM_EMU256
(eine 2x entrollte Version von wasm128, aktiviert, wenn HWY_WANT_WASM2
definiert ist. Dies wird weiterhin unterstützt, bis es möglicherweise durch eine zukünftige Version von WASM ersetzt wird.);SSE2
SSSE3
(~Intel Core)SSE4
(~Nehalem, enthält auch AES + CLMUL).AVX2
(~Haswell, enthält auch BMI2 + F16 + FMA)AVX3
(~Skylake, AVX-512F/BW/CD/DQ/VL)AVX3_DL
(~Icelake, enthält BitAlg + CLMUL + GFNI + VAES + VBMI + VBMI2 + VNNI + VPOPCNT; erfordert Opt-in durch Definition von HWY_WANT_AVX3_DL
, es sei denn, es wird für den statischen Versand kompiliert),AVX3_ZEN4
(wie AVX3_DL, aber optimiert für AMD Zen4; erfordert Opt-in durch Definition HWY_WANT_AVX3_ZEN4
beim Kompilieren für den statischen Versand, ist aber standardmäßig für den Laufzeit-Versand aktiviert),AVX3_SPR
(~Sapphire Rapids, einschließlich AVX-512FP16)Unsere Richtlinie besagt, dass Ziele, sofern nicht anders angegeben, weiterhin unterstützt werden, solange sie mit derzeit unterstütztem Clang oder GCC (übergreifend) kompiliert und mit QEMU getestet werden können. Wenn das Ziel mit LLVM-Trunk kompiliert und mit unserer QEMU-Version ohne zusätzliche Flags getestet werden kann, kann es in unsere Infrastruktur für kontinuierliche Tests aufgenommen werden. Andernfalls wird das Ziel vor der Veröffentlichung mit ausgewählten Versionen/Konfigurationen von Clang und GCC manuell getestet.
SVE wurde ursprünglich mit farm_sve getestet (siehe Danksagungen).
Highway-Releases zielen darauf ab, dem semver.org-System (MAJOR.MINOR.PATCH) zu folgen und MINOR nach abwärtskompatiblen Ergänzungen und PATCH nach abwärtskompatiblen Korrekturen zu erhöhen. Wir empfehlen die Verwendung von Releases (anstelle des Git-Tipps), da diese ausführlicher getestet werden, siehe unten.
Die aktuelle Version 1.0 signalisiert einen verstärkten Fokus auf Abwärtskompatibilität. Anwendungen, die dokumentierte Funktionen nutzen, bleiben mit zukünftigen Updates kompatibel, die dieselbe Hauptversionsnummer haben.
Kontinuierliche Integrationstests werden mit einer aktuellen Version von Clang (läuft auf nativem x86 oder QEMU für RISC-V und Arm) und MSVC 2019 (v19.28, läuft auf nativem x86) erstellt.
Vor Veröffentlichungen testen wir auch auf x86 mit Clang und GCC und Armv7/8 über GCC-Cross-Compile. Einzelheiten finden Sie im Testprozess.
Das contrib
-Verzeichnis enthält SIMD-bezogene Dienstprogramme: eine Bildklasse mit ausgerichteten Zeilen, eine Mathematikbibliothek (16 bereits implementierte Funktionen, hauptsächlich Trigonometrie) und Funktionen zur Berechnung von Skalarprodukten und zum Sortieren.
Wenn Sie nur x86-Unterstützung benötigen, können Sie auch die VCL-Vektorklassenbibliothek von Agner Fog verwenden. Es enthält viele Funktionen, einschließlich einer vollständigen Mathematikbibliothek.
Wenn Sie vorhandenen Code haben, der x86/NEON-Intrinsics verwendet, könnten Sie an SIMDe interessiert sein, das diese Intrinsics mithilfe der Intrinsics anderer Plattformen oder der Autovektorisierung emuliert.
Dieses Projekt verwendet CMake zum Generieren und Erstellen. In einem Debian-basierten System können Sie es installieren über:
sudo apt install cmake
Die Komponententests von Highway verwenden Googletest. Standardmäßig lädt CMake von Highway diese Abhängigkeit zum Zeitpunkt der Konfiguration herunter. Sie können dies vermeiden, indem Sie die CMake-Variable HWY_SYSTEM_GTEST
auf ON setzen und gtest separat installieren:
sudo apt install libgtest-dev
Alternativ können Sie HWY_TEST_STANDALONE=1
definieren und alle Vorkommen von gtest_main
in jeder BUILD-Datei entfernen. Dann vermeiden Tests die Abhängigkeit von GUnit.
Das Ausführen von Cross-Compiled-Tests erfordert Unterstützung vom Betriebssystem, die unter Debian durch das Paket qemu-user-binfmt
bereitgestellt wird.
Um Highway als gemeinsam genutzte oder statische Bibliothek zu erstellen (abhängig von BUILD_SHARED_LIBS), kann der standardmäßige CMake-Workflow verwendet werden:
mkdir -p build && cd build
cmake ..
make -j && make test
Oder Sie können run_tests.sh
( run_tests.bat
unter Windows) ausführen.
Bazel wird auch für den Bau unterstützt, ist jedoch nicht so weit verbreitet/getestet.
Beim Erstellen für Armv7 müssen Sie aufgrund einer Einschränkung aktueller Compiler -DHWY_CMAKE_ARM7:BOOL=ON
zur CMake-Befehlszeile hinzufügen. siehe #834 und #1032. Wir verstehen, dass derzeit daran gearbeitet wird, diese Einschränkung aufzuheben.
Der Aufbau auf 32-Bit x86 wird offiziell nicht unterstützt und AVX2/3 sind dort standardmäßig deaktiviert. Beachten Sie, dass Johnplatts die Highway-Tests erfolgreich auf 32-Bit x86, einschließlich AVX2/3, auf GCC 7/8 und Clang 8/11/12 erstellt und ausgeführt hat. Unter Ubuntu 22.04 erfordern Clang 11 und 12, jedoch nicht spätere Versionen, zusätzliche Compiler-Flags -m32 -isystem /usr/i686-linux-gnu/include
. Clang 10 und früher erfordern das oben genannte Plus -isystem /usr/i686-linux-gnu/include/c++/12/i686-linux-gnu
. Siehe #1279.
Highway ist jetzt in vcpkg verfügbar
vcpkg install highway
Der Highway-Port in vcpkg wird von Microsoft-Teammitgliedern und Community-Mitwirkenden auf dem neuesten Stand gehalten. Wenn die Version veraltet ist, erstellen Sie bitte einen Issue oder Pull Request im vcpkg-Repository.
Sie können den benchmark
in „examples/“ als Ausgangspunkt verwenden.
Auf einer Kurzreferenzseite werden alle Operationen und ihre Parameter kurz aufgelistet, und die Anweisungsmatrix gibt die Anzahl der Anweisungen pro Operation an.
Die FAQ beantworten Fragen zur Portabilität, zum API-Design und wo Sie weitere Informationen finden.
Für maximale Leistungsportabilität empfehlen wir, wann immer möglich, vollständige SIMD-Vektoren zu verwenden. Um sie zu erhalten, übergeben Sie ein ScalableTag
-Tag (oder entsprechend HWY_FULL(float)
) an Funktionen wie Zero/Set/Load
. Für Anwendungsfälle, die eine Obergrenze für die Lanes erfordern, gibt es zwei Alternativen:
Geben Sie für bis zu N
Spuren CappedTag
oder das Äquivalent HWY_CAPPED(T, N)
an. Die tatsächliche Anzahl der Spuren ist N
abgerundet auf die nächste Zweierpotenz, z. B. 4, wenn N
5 ist, oder 8, wenn N
8 ist. Dies ist nützlich für Datenstrukturen wie eine schmale Matrix. Eine Schleife ist weiterhin erforderlich, da Vektoren tatsächlich weniger als N
Spuren haben können.
Für genau eine Potenz von zwei N
Spuren geben Sie FixedTag
an. Das größte unterstützte N
hängt vom Ziel ab, beträgt aber garantiert mindestens 16/sizeof(T)
.
Aufgrund von ADL-Einschränkungen muss der Benutzercode, der Highway-Ops aufruft, entweder:
namespace hwy { namespace HWY_NAMESPACE {
; odernamespace hn = hwy::HWY_NAMESPACE; hn::Add()
; oderusing hwy::HWY_NAMESPACE::Add;
. Darüber hinaus muss jeder Funktion, die Highway-Operationen aufruft (z. B. Load
), entweder HWY_ATTR
vorangestellt werden ODER sich zwischen HWY_BEFORE_NAMESPACE()
und HWY_AFTER_NAMESPACE()
befinden. Lambda-Funktionen erfordern derzeit HWY_ATTR
vor ihrer öffnenden Klammer.
Verwenden Sie weder Namespace-Scope noch static
Initialisierer für SIMD-Vektoren, da dies zu SIGILL führen kann, wenn Sie Runtime Dispatch verwenden und der Compiler einen Initialisierer auswählt, der für ein Ziel kompiliert wurde, das von der aktuellen CPU nicht unterstützt wird. Stattdessen sollten über Set
initialisierte Konstanten im Allgemeinen lokale (const) Variablen sein.
Die Einstiegspunkte in den Code mithilfe von Highway unterscheiden sich geringfügig, je nachdem, ob statischer oder dynamischer Versand verwendet wird. In beiden Fällen empfehlen wir, dass die Funktion der obersten Ebene einen oder mehrere Zeiger auf Arrays und nicht auf zielspezifische Vektortypen empfängt.
Für den statischen Versand ist HWY_TARGET
das beste verfügbare Ziel unter HWY_BASELINE_TARGETS
, d. h. diejenigen, die vom Compiler verwendet werden dürfen (siehe Kurzreferenz). Funktionen in HWY_NAMESPACE
können mit HWY_STATIC_DISPATCH(func)(args)
innerhalb desselben Moduls aufgerufen werden, in dem sie definiert sind. Sie können die Funktion von anderen Modulen aus aufrufen, indem Sie sie in eine reguläre Funktion einschließen und die reguläre Funktion in einem Header deklarieren.
Für den dynamischen Versand wird über das HWY_EXPORT
Makro eine Tabelle mit Funktionszeigern generiert, die von HWY_DYNAMIC_DISPATCH(func)(args)
verwendet wird, um den besten Funktionszeiger für die unterstützten Ziele der aktuellen CPU aufzurufen. Für jedes Ziel wird automatisch ein Modul in HWY_TARGETS
kompiliert (siehe Kurzreferenz), wenn HWY_TARGET_INCLUDE
definiert und foreach_target.h
enthalten ist. Beachten Sie, dass der erste Aufruf von HWY_DYNAMIC_DISPATCH
oder jeder Aufruf des vom ersten Aufruf von HWY_DYNAMIC_POINTER
zurückgegebenen Zeigers einen gewissen CPU-Erkennungsaufwand mit sich bringt. Sie können dies verhindern, indem Sie vor jedem Aufruf von HWY_DYNAMIC_*
Folgendes aufrufen: hwy::GetChosenTarget().Update(hwy::SupportedTargets());
.
Siehe auch eine separate Einführung zum dynamischen Versand von @kfjahnke.
Bei Verwendung des dynamischen Versands wird foreach_target.h
aus Übersetzungseinheiten (.cc-Dateien) und nicht aus Headern eingebunden. Header, die Vektorcode enthalten, der von mehreren Übersetzungseinheiten gemeinsam genutzt wird, erfordern einen speziellen Include-Schutz, zum Beispiel der folgende aus examples/skeleton-inl.h
:
#if defined(HIGHWAY_HWY_EXAMPLES_SKELETON_INL_H_) == defined(HWY_TARGET_TOGGLE)
#ifdef HIGHWAY_HWY_EXAMPLES_SKELETON_INL_H_
#undef HIGHWAY_HWY_EXAMPLES_SKELETON_INL_H_
#else
#define HIGHWAY_HWY_EXAMPLES_SKELETON_INL_H_
#endif
#include "hwy/highway.h"
// Your vector code
#endif
Konventionell nennen wir solche Header -inl.h
, da ihr Inhalt (häufig Funktionsvorlagen) normalerweise inline ist.
Anwendungen sollten mit aktivierten Optimierungen kompiliert werden. Ohne Inlining kann der SIMD-Code um den Faktor 10 bis 100 langsamer werden. Für Clang und GCC ist -O2
im Allgemeinen ausreichend.
Für MSVC empfehlen wir die Kompilierung mit /Gv
, damit nicht inline Funktionen Vektorargumente in Registern übergeben können. Wenn Sie beabsichtigen, das AVX2-Ziel zusammen mit Vektoren halber Breite zu verwenden (z. B. für PromoteTo
), ist es auch wichtig, mit /arch:AVX2
zu kompilieren. Dies scheint die einzige Möglichkeit zu sein, VEX-codierte SSE-Anweisungen auf MSVC zuverlässig zu generieren. Manchmal generiert MSVC VEX-codierte SSE-Anweisungen, wenn sie mit AVX gemischt werden, aber nicht immer, siehe DevCom-10618264. Andernfalls kann das Mischen von VEX-codierten AVX2-Anweisungen und Nicht-VEX-SSE zu erheblichen Leistungseinbußen führen. Leider erfordert die resultierende Binärdatei mit der Option /arch:AVX2
. Beachten Sie, dass für Clang und GCC kein solches Flag erforderlich ist, da sie zielspezifische Attribute unterstützen, die wir verwenden, um eine ordnungsgemäße VEX-Codegenerierung für AVX2-Ziele sicherzustellen.
Bei der Vektorisierung einer Schleife ist eine wichtige Frage, ob und wie mit einer Anzahl von Iterationen („trip count“, bezeichnet als count
) umgegangen werden soll, die die Vektorgröße N = Lanes(d)
nicht gleichmäßig aufteilen. Beispielsweise kann es erforderlich sein, das Schreiben über das Ende eines Arrays hinaus zu vermeiden.
In diesem Abschnitt bezeichnen T
den Elementtyp und d = ScalableTag
. Angenommen, der Schleifenkörper wird als Funktion template
angegeben.
„Strip-Mining“ ist eine Technik zur Vektorisierung einer Schleife durch Transformation in eine äußere und eine innere Schleife, sodass die Anzahl der Iterationen in der inneren Schleife der Vektorbreite entspricht. Anschließend wird die innere Schleife durch Vektoroperationen ersetzt.
Highway bietet mehrere Strategien zur Schleifenvektorisierung:
Stellen Sie sicher, dass alle Ein-/Ausgänge gepolstert sind. Dann ist die (äußere) Schleife einfach
for (size_t i = 0; i < count; i += N) LoopBody(d, i, 0);
Hier werden der Vorlagenparameter und das zweite Funktionsargument nicht benötigt.
Dies ist die bevorzugte Option, es sei denn, N
liegt im Tausenderbereich und Vektoroperationen werden mit langen Latenzen per Pipeline verarbeitet. Dies war in den 90er Jahren bei Supercomputern der Fall, aber heutzutage sind ALUs billig und wir sehen, dass die meisten Implementierungen Vektoren in 1, 2 oder 4 Teile aufteilen, sodass die Verarbeitung ganzer Vektoren nur geringe Kosten verursacht, selbst wenn wir nicht alle ihre Spuren benötigen. Dies vermeidet in der Tat die (potenziell hohen) Kosten für Prädikation oder teilweises Laden/Speichern auf älteren Zielen und dupliziert keinen Code.
Verarbeiten Sie ganze Vektoren und fügen Sie zuvor verarbeitete Elemente in den letzten Vektor ein:
for (size_t i = 0; i < count; i += N) LoopBody(d, HWY_MIN(i, count - N), 0);
Dies ist die zweite bevorzugte Option, sofern count >= N
und LoopBody
idempotent ist. Einige Elemente werden möglicherweise zweimal verarbeitet, aber ein einzelner Codepfad und eine vollständige Vektorisierung lohnen sich normalerweise. Auch wenn count < N
ist, ist es normalerweise sinnvoll, Ein-/Ausgänge auf N
aufzufüllen.
Verwenden Sie die Transform*
-Funktionen in hwy/contrib/algo/transform-inl.h. Dies kümmert sich um die Schleifen- und Restbehandlung und Sie definieren einfach eine generische Lambda-Funktion (C++14) oder einen Funktor, der den aktuellen Vektor vom Eingabe-/Ausgabe-Array sowie optional Vektoren von bis zu zwei zusätzlichen Eingabe-Arrays empfängt und zurückgibt Der Wert, der in das Eingabe-/Ausgabearray geschrieben werden soll.
Hier ist ein Beispiel für die Implementierung der BLAS-Funktion SAXPY ( alpha * x + y
):
Transform1(d, x, n, y, [](auto d, const auto v, const auto v1) HWY_ATTR {
return MulAdd(Set(d, alpha), v, v1);
});
Verarbeiten Sie ganze Vektoren wie oben, gefolgt von einer Skalarschleife:
size_t i = 0;
for (; i + N <= count; i += N) LoopBody(d, i, 0);
for (; i < count; ++i) LoopBody(CappedTag(), i, 0);
Der Vorlagenparameter und die zweiten Funktionsargumente werden wiederum nicht benötigt.
Dies vermeidet die Duplizierung von Code und ist sinnvoll, wenn count
groß ist. Wenn count
klein ist, ist die zweite Schleife möglicherweise langsamer als die nächste Option.
Verarbeiten Sie ganze Vektoren wie oben, gefolgt von einem einzelnen Aufruf eines modifizierten LoopBody
mit Maskierung:
size_t i = 0;
for (; i + N <= count; i += N) {
LoopBody(d, i, 0);
}
if (i < count) {
LoopBody(d, i, count - i);
}
Jetzt können der Vorlagenparameter und das dritte Funktionsargument in LoopBody
verwendet werden, um die ersten num_remaining
Spuren von v
nicht atomar mit dem vorherigen Speicherinhalt an nachfolgenden Positionen zu „vermischen“: BlendedStore(v, FirstN(d, num_remaining), d, pointer);
. In ähnlicher Weise lädt MaskedLoad(FirstN(d, num_remaining), d, pointer)
die ersten num_remaining
Elemente und gibt in anderen Spuren Null zurück.
Dies ist eine gute Standardeinstellung, wenn es nicht möglich ist, sicherzustellen, dass Vektoren aufgefüllt werden, ist aber nur sicher #if !HWY_MEM_OPS_MIGHT_FAULT
! Im Gegensatz zur Skalarschleife ist nur eine einzige abschließende Iteration erforderlich. Es wird erwartet, dass sich die erhöhte Codegröße aus zwei Schleifenkörpern lohnt, da dadurch die Kosten für die Maskierung in allen Iterationen bis auf die letzte Iteration vermieden werden.
Wir haben farm-sve von Berenger Bramas verwendet; Es hat sich als nützlich erwiesen, um den SVE-Port auf einem x86-Entwicklungscomputer zu überprüfen.
Dies ist kein offiziell unterstütztes Google-Produkt. Kontakt: [email protected]