Schnelle, parallele Anwendungen mit WebAssembly SIMD
SIMD steht für Single Instruction, Multiple Data. SIMD-Anweisungen sind eine spezielle Klasse von Anweisungen, die Datenparallelität in Anwendungen ausnutzen, indem sie gleichzeitig dieselbe Operation auf mehreren Datenelementen ausführen. Rechenintensive Anwendungen wie Audio-/Video-Codecs, Bildverarbeitungsprogramme sind Beispiele für Anwendungen, die SIMD-Anweisungen nutzen, um die Leistung zu verbessern. Die meisten modernen Architekturen unterstützen einige Varianten von SIMD-Anweisungen.
Der WebAssembly SIMD-Vorschlag definiert einen portablen, leistungsfähigen Unterbereich von SIMD-Operationen, die auf den meisten modernen Architekturen verfügbar sind. Dieser Vorschlag hat viele Elemente aus dem SIMD.js Vorschlag übernommen, der ursprünglich aus der Dart SIMD Spezifikation abgeleitet wurde. Der SIMD.js-Vorschlag war eine API, die bei TC39 mit neuen Typen und Funktionen für SIMD-Berechnungen vorgeschlagen wurde, jedoch wurde dieser zugunsten einer transparenteren Unterstützung von SIMD-Operationen in WebAssembly archiviert. Der WebAssembly SIMD Vorschlag wurde eingeführt, um Browsern zu ermöglichen, die Datenebenenparallelität mithilfe der zugrunde liegenden Hardware zu nutzen.
WebAssembly SIMD Vorschlag
Das übergeordnete Ziel des WebAssembly SIMD Vorschlags ist es, Vektoroperationen zur WebAssembly-Spezifikation hinzuzufügen, in einer Weise, die portable Leistung garantiert.
Die Menge der SIMD-Anweisungen ist groß und unterscheidet sich je nach Architektur. Die im WebAssembly SIMD Vorschlag enthaltenen Operationen bestehen aus Operationen, die auf einer Vielzahl von Plattformen gut unterstützt werden und sich als leistungsfähig erwiesen haben. Zu diesem Zweck beschränkt sich der aktuelle Vorschlag auf die Standardisierung von Fixed-Width 128-Bit SIMD-Operationen.
Der aktuelle Vorschlag führt einen neuen v128
Werttyp hinzu und eine Reihe neuer Operationen, die auf diesem Typ arbeiten. Die Kriterien zur Bestimmung dieser Operationen sind:
- Die Operationen sollten auf mehreren modernen Architekturen gut unterstützt sein.
- Leistungsgewinne sollten in mehreren relevanten Architekturen innerhalb einer Anweisungsgruppe positiv sein.
- Das gewählte Satz von Operationen sollte Leistungseinbrüche, falls vorhanden, minimieren.
Der Vorschlag befindet sich nun im abgeschlossenen Zustand (Phase 4), sowohl V8 als auch die Toolchain verfügen über funktionale Implementierungen.
Aktivieren der SIMD-Unterstützung
Feature-Erkennung
Zunächst einmal, beachten Sie dass SIMD eine neue Funktion ist und nicht in allen Browsern mit WebAssembly-Unterstützung verfügbar ist. Sie können auf der Website webassembly.org herausfinden, welche Browser neue WebAssembly-Funktionen unterstützen.
Um sicherzustellen, dass alle Benutzer Ihre Anwendung laden können, müssen Sie zwei verschiedene Versionen - eine mit SIMD aktiviert und eine ohne - erstellen und die entsprechende Version abhängig von den Ergebnissen der Feature-Erkennung laden. Um SIMD zur Laufzeit zu erkennen, können Sie die wasm-feature-detect
Bibliothek verwenden und das entsprechende Modul wie folgt laden:
import { simd } from 'wasm-feature-detect';
(async () => {
const hasSIMD = await simd();
const module = await (
hasSIMD
? import('./module-with-simd.js')
: import('./module-without-simd.js')
);
// …jetzt verwende `module` wie gewohnt
})();
Um zu erfahren, wie man Code mit SIMD-Unterstützung erstellt, schauen Sie sich den Abschnitt unten an.
SIMD-Unterstützung in Browsern
WebAssembly SIMD-Unterstützung ist standardmäßig ab Chrome 91 verfügbar. Stellen Sie sicher, dass Sie die neueste Version der Toolchain wie unten beschrieben nutzen, sowie die neueste wasm-feature-detect, um Engines zu erkennen, die die endgültige Version der Spezifikation unterstützen. Wenn etwas nicht richtig aussieht, bitte melden Sie einen Fehler.
WebAssembly SIMD wird auch in Firefox 89 und höher unterstützt.
Erstellen mit SIMD-Unterstützung
Erstellen von C / C++ zum Ziel SIMD
Die SIMD-Unterstützung von WebAssembly hängt von der Verwendung eines aktuellen Builds von clang mit aktiviertem WebAssembly LLVM-Backend ab. Emscripten unterstützt ebenfalls den WebAssembly SIMD Vorschlag. Installieren und aktivieren Sie die neueste
Verteilung von emscripten mit emsdk, um die SIMD-Funktionen zu nutzen.
./emsdk install latest
./emsdk activate latest
Es gibt mehrere Möglichkeiten, SIMD-Code zu aktivieren, wenn Sie Ihre Anwendung so portieren, dass sie SIMD verwendet. Nachdem die neueste Upstream-Version von Emscripten installiert wurde, kompilieren Sie mit Emscripten und übergeben Sie das -msimd128
-Flag, um SIMD zu aktivieren.
emcc -msimd128 -O3 foo.c -o foo.js
Anwendungen, die bereits auf WebAssembly portiert wurden, können dank der Autovektorisierungs-Optimierungen von LLVM von SIMD profitieren, ohne den Quellcode ändern zu müssen.
Diese Optimierungen können Schleifen, die bei jeder Iteration arithmetische Operationen durchführen, automatisch in äquivalente Schleifen transformieren, die dieselben arithmetischen Operationen mit mehreren Eingaben gleichzeitig unter Verwendung von SIMD-Befehlen ausführen. Die Autovektorisierer von LLVM sind standardmäßig auf den Optimierungsstufen -O2
und -O3
aktiviert, wenn das -msimd128
-Flag angegeben wird.
Betrachten Sie beispielsweise die folgende Funktion, die die Elemente zweier Eingabe-Arrays miteinander multipliziert und die Ergebnisse in einem Ausgabe-Array speichert.
void multiply_arrays(int* out, int* in_a, int* in_b, int size) {
for (int i = 0; i < size; i++) {
out[i] = in_a[i] * in_b[i];
}
}
Ohne das -msimd128
-Flag gibt der Compiler diese WebAssembly-Schleife aus:
(loop
(i32.store
… Adresse in `out` abrufen …
(i32.mul
(i32.load … Adresse in `in_a` abrufen …)
(i32.load … Adresse in `in_b` abrufen …)
…
)
Wird jedoch das -msimd128
-Flag verwendet, wandelt der Autovektorisierer dies in Code um, der die folgende Schleife enthält:
(loop
(v128.store align=4
… Adresse in `out` abrufen …
(i32x4.mul
(v128.load align=4 … Adresse in `in_a` abrufen …)
(v128.load align=4 … Adresse in `in_b` abrufen …)
…
)
)
Die Schleifenkörperstruktur bleibt gleich, jedoch werden SIMD-Befehle verwendet, um vier Elemente gleichzeitig innerhalb des Schleifenkörpers zu laden, zu multiplizieren und zu speichern.
Für eine feinere Steuerung der vom Compiler generierten SIMD-Befehle können Sie die Kopfzeile wasm_simd128.h
einbeziehen, die eine Reihe von Intrinsics definiert. Intrinsics sind spezielle Funktionen, die vom Compiler in die entsprechenden WebAssembly-SIMD-Befehle umgewandelt werden, sofern keine weiteren Optimierungen durchgeführt werden können.
Hier ist ein Beispiel für denselben vorherigen Code, der manuell neu geschrieben wurde, um die SIMD-Intrinsics zu verwenden.
#include <wasm_simd128.h>
void multiply_arrays(int* out, int* in_a, int* in_b, int size) {
for (int i = 0; i < size; i += 4) {
v128_t a = wasm_v128_load(&in_a[i]);
v128_t b = wasm_v128_load(&in_b[i]);
v128_t prod = wasm_i32x4_mul(a, b);
wasm_v128_store(&out[i], prod);
}
}
Dieser manuell umgeschriebene Code setzt voraus, dass die Eingabe- und Ausgabearrays ausgerichtet sind, sich nicht überschneiden und die Größe ein Vielfaches von vier ist. Der Autovektorisierer kann diese Annahmen nicht treffen und muss zusätzlichen Code generieren, um die Fälle zu behandeln, in denen diese Annahmen nicht zutreffen. Daher ist handgeschriebener SIMD-Code oft kleiner als autovektorisierter SIMD-Code.
Bestehende C-/C++-Projekte plattformübergreifend kompilieren
Viele bestehende Projekte unterstützen bereits SIMD, wenn sie andere Plattformen anvisieren, insbesondere SSE- und AVX-Instruktionen auf x86/x86-64-Plattformen sowie NEON-Instruktionen auf ARM-Plattformen. Es gibt zwei übliche Ansätze für deren Implementierung.
Der erste Ansatz verwendet Assembly-Dateien, die sich um SIMD-Operationen kümmern und während des Build-Prozesses zusammen mit C-/C++-Code verlinkt werden. Die Assembly-Syntax und -Instruktionen sind stark plattformspezifisch und nicht portabel. Um SIMD zu verwenden, müssen solche Projekte WebAssembly als zusätzlichen unterstützten Zieltyp hinzufügen und die entsprechenden Funktionen unter Verwendung von entweder WebAssembly-Textformaten oder den oben beschriebenen Intrinsics neu implementieren.
Ein anderer üblicher Ansatz besteht darin, SSE-/SSE2-/AVX-/NEON-Intrinsics direkt im C-/C++-Code zu verwenden. Hierbei kann Emscripten helfen. Emscripten stellt kompatible Headerdateien und eine Emulationsebene bereit für all diese Befehlssätze. Die Emulationsebene übersetzt sie entweder direkt in entsprechende Wasm-Intrinsics oder, falls nicht möglich, in skalaren Code.
Um solche Projekte plattformübergreifend zu kompilieren, aktivieren Sie zunächst SIMD über projektspezifische Konfigurationsflags, z. B. ./configure --enable-simd
, damit das -msse
, -msse2
, -mavx
oder -mfpu=neon
an den Compiler übergeben wird und die entsprechenden Intrinsics aufgerufen werden. Übergeben Sie anschließend zusätzlich -msimd128
, um WebAssembly-SIMD zu aktivieren, entweder durch Verwendung von CFLAGS=-msimd128 make …
/ CXXFLAGS="-msimd128 make …
oder durch direkte Änderung der Build-Konfiguration beim Anvisieren von Wasm.
Rust kompilieren, um SIMD anzusteuern
Beim Kompilieren von Rust-Code für WebAssembly-SIMD müssen Sie dieselbe LLVM-Funktion simd128
wie oben bei Emscripten aktivieren.
Wenn Sie die rustc
-Flags direkt oder über die Umgebungsvariable RUSTFLAGS
steuern können, übergeben Sie -C target-feature=+simd128
:
rustc … -C target-feature=+simd128 -o out.wasm
oder
RUSTFLAGS="-C target-feature=+simd128" cargo build
Wie in Clang / Emscripten sind die Autovektorisierer von LLVM standardmäßig für optimierten Code aktiviert, wenn das simd128
-Feature aktiviert ist.
Zum Beispiel das Rust-Äquivalent des obigen Beispiels multiply_arrays
pub fn multiply_arrays(out: &mut [i32], in_a: &[i32], in_b: &[i32]) {
in_a.iter()
.zip(in_b)
.zip(out)
.for_each(|((a, b), dst)| {
*dst = a * b;
});
}
würde für den ausgerichteten Teil der Eingaben ähnlichen autovektorisierten Code erzeugen.
Um manuelle Kontrolle über die SIMD-Operationen zu haben, können Sie die Nightly-Toolchain verwenden, das Rust-Feature wasm_simd
aktivieren und die Intrinsics direkt aus dem std::arch::wasm32
-Namespace aufrufen:
#![feature(wasm_simd)]
use std::arch::wasm32::*;
pub unsafe fn multiply_arrays(out: &mut [i32], in_a: &[i32], in_b: &[i32]) {
in_a.chunks(4)
.zip(in_b.chunks(4))
.zip(out.chunks_mut(4))
.for_each(|((a, b), dst)| {
let a = v128_load(a.as_ptr() as *const v128);
let b = v128_load(b.as_ptr() as *const v128);
let prod = i32x4_mul(a, b);
v128_store(dst.as_mut_ptr() as *mut v128, prod);
});
}
Alternativ können Sie ein Hilfs-Crate wie packed_simd
verwenden, das über SIMD-Implementierungen auf verschiedenen Plattformen abstrahiert.
Überzeugende Anwendungsfälle
Der WebAssembly-SIMD-Vorschlag zielt darauf ab, rechenintensive Anwendungen wie Audio-/Videocodecs, Bildverarbeitungsanwendungen, kryptografische Anwendungen usw. zu beschleunigen. Derzeit wird WebAssembly SIMD experimentell in weit verbreiteten Open-Source-Projekten wie Halide, OpenCV.js und XNNPACK unterstützt.
Einige interessante Demos stammen aus dem MediaPipe-Projekt des Google-Forschungsteams.
Laut ihrer Beschreibung ist MediaPipe ein Framework zum Erstellen multimodaler (z. B. Video, Audio, beliebige Zeitreihen-Daten) angewandter maschineller Lernpipelines. Es gibt auch eine Webversion!
Eines der optisch ansprechendsten Demos, bei denen es einfach ist, den Performanceunterschied durch SIMD zu beobachten, ist ein reines CPU (nicht GPU) Build eines Handverfolgungssystems. Ohne SIMD erreicht man auf einem modernen Laptop nur etwa 14-15 FPS (Bilder pro Sekunde), während man mit SIMD, aktiviert in Chrome Canary, eine viel flüssigere Erfahrung mit 38-40 FPS erhält.
Ein weiteres interessantes Set von Demos, das SIMD für eine flüssige Erfahrung nutzt, stammt von OpenCV - einer beliebten Computer-Vision-Bibliothek, die ebenfalls zu WebAssembly kompiliert werden kann. Sie sind verfügbar über Link oder Sie können sich die vorab aufgezeichneten Versionen unten ansehen:
Zukünftige Arbeiten
Der aktuelle Fixed-Width-SIMD-Vorschlag befindet sich in Phase 4, daher wird er als abgeschlossen betrachtet.
Einige Untersuchungen zu zukünftigen SIMD-Erweiterungen sind in den Vorschlägen Relaxed SIMD und Flexible Vectors begonnen worden, die sich zum Zeitpunkt des Schreibens in Phase 1 befinden.