Быстрые, параллельные приложения с WebAssembly SIMD
SIMD означает Одиночная инструкция, множество данных. SIMD-инструкции представляют собой специальный класс инструкций, которые используют параллелизм данных в приложениях, выполняя одну и ту же операцию одновременно на нескольких элементах данных. Приложения с высокой интенсивностью вычислений, такие как аудио/видео кодеки, процессоры изображений, являются примерами приложений, которые используют SIMD-инструкции для ускорения производительности. Большинство современных архитектур поддерживают некоторые варианты SIMD-инструкций.
Предложение WebAssembly SIMD определяет переносимый, производительный подмножество SIMD-операций, доступных на большинстве современных архитектур. Этот проект позаимствовал многие элементы из предложения SIMD.js, которое, в свою очередь, изначально было получено из спецификации Dart SIMD. Предложение SIMD.js было API, предложенным в TC39 с новыми типами и функциями для выполнения SIMD-вычислений, но оно было архивировано в пользу более прозрачной поддержки SIMD-операций в WebAssembly. Предложение WebAssembly SIMD было представлено как способ использования браузерами параллелизма данных с применением аппаратного обеспечения.
Предложение WebAssembly SIMD
Высокоуровневая цель предложения WebAssembly SIMD — ввести векторные операции в спецификацию WebAssembly таким образом, чтобы гарантировать переносимое выполнение.
Набор SIMD-инструкций обширен и варьируется в зависимости от архитектур. Набор операций, включенных в предложение WebAssembly SIMD, состоит из операций, которые хорошо поддерживаются на большом количестве платформ и доказали свою эффективность. В настоящее время предложение ограничено стандартизацией фиксированной ширины 128-битных SIMD-операций.
В текущем предложении вводится новый тип значения v128
и ряд новых операций, которые работают с этим типом. Критерии, используемые для определения этих операций:
- Операции должны быть хорошо поддержаны на нескольких современных архитектурах.
- Прирост производительности должен быть положительным на нескольких соответствующих архитектурах внутри группы инструкций.
- Выбранный набор операций должен минимизировать производственные провалы, если они имеются.
Предложение сейчас находится в финализированном состоянии (фаза 4), и V8, и инструментарий имеют рабочие реализации.
Включение поддержки SIMD
Обнаружение функции
Прежде всего, отметьте, что SIMD — это новая функция и пока доступно не во всех браузерах с поддержкой WebAssembly. Вы можете найти, какие браузеры поддерживают новые функции WebAssembly, на сайте webassembly.org.
Чтобы гарантировать, что все пользователи смогут загрузить ваше приложение, вам нужно создать две разные версии — одну с включенным SIMD и одну без него — и загрузить соответствующую версию в зависимости от результатов обнаружения функции. Чтобы обнаружить SIMD во время выполнения, вы можете использовать библиотеку wasm-feature-detect
и загрузить соответствующий модуль следующим образом:
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')
);
// …теперь используйте `module` как обычно
})();
Чтобы узнать о создании кода с поддержкой SIMD, ознакомьтесь с разделом ниже.
Поддержка SIMD в браузерах
Поддержка WebAssembly SIMD доступна по умолчанию начиная с Chrome 91. Убедитесь, что вы используете последнюю версию инструментария, как описано ниже, а также новейший wasm-feature-detect для обнаружения движков, которые поддерживают окончательную версию спецификации. Если что-то выглядит неправильно, пожалуйста, сообщите о проблеме.
WebAssembly SIMD также поддерживается в Firefox 89 и выше.
Создание с поддержкой SIMD
Создание C / C++ с целью SIMD
Поддержка SIMD в WebAssembly зависит от использования последней сборки clang с включенным бэкэндом LLVM для WebAssembly. Emscripten также поддерживает предложение WebAssembly SIMD. Установите и активируйте latest
дистрибуцию Emscripten с помощью emsdk, чтобы использовать функции SIMD.
./emsdk install latest
./emsdk activate latest
Существует несколько различных способов включения генерации SIMD-кода при портировании вашего приложения для использования SIMD. После установки последней версии emscripten выполните компиляцию с использованием emscripten и передайте флаг -msimd128
, чтобы включить SIMD.
emcc -msimd128 -O3 foo.c -o foo.js
Приложения, которые уже были портированы для использования WebAssembly, могут получить преимущества от SIMD без изменения исходного кода благодаря оптимизациям автосборщика LLVM.
Эти оптимизации могут автоматически преобразовывать циклы, выполняющие арифметические операции при каждой итерации, в эквивалентные циклы, выполняющие те же арифметические операции над несколькими входными данными одновременно с использованием команд SIMD. Автосборщики LLVM включены по умолчанию на уровнях оптимизации -O2
и -O3
, когда передан флаг -msimd128
.
Например, рассмотрим следующую функцию, которая умножает элементы двух входных массивов и сохраняет результаты в выходном массиве.
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];
}
}
Без передачи флага -msimd128
, компилятор генерирует следующий WebAssembly-цикл:
(loop
(i32.store
… получить адрес в `out` …
(i32.mul
(i32.load … получить адрес в `in_a` …)
(i32.load … получить адрес в `in_b` …)
…
)
Но при использовании флага -msimd128
автосборщик преобразует это в код, включающий следующий цикл:
(loop
(v128.store align=4
… получить адрес в `out` …
(i32x4.mul
(v128.load align=4 … получить адрес в `in_a` …)
(v128.load align=4 … получить адрес в `in_b` …)
…
)
)
Тело цикла имеет ту же структуру, но используются инструкции SIMD для загрузки, умножения и сохранения четырех элементов одновременно внутри тела цикла.
Для более тонкого контроля над инструкциями SIMD, создаваемыми компилятором, подключите заголовочный файл wasm_simd128.h
, который определяет набор встроенных функций. Встроенные функции — это специальные функции, которые компилятор преобразует в соответствующие инструкции SIMD для WebAssembly, если он не может выполнить дальнейшую оптимизацию.
Например, вот та же функция, переписанная вручную с использованием встроенных функций SIMD.
#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);
}
}
Этот вручную переписанный код предполагает, что входные и выходные массивы выровнены и не пересекаются, а размер кратен четырем. Автосборщик не может сделать такие предположения и должен генерировать дополнительный код для обработки случаев, когда они неверны, поэтому вручную написанный SIMD-код часто оказывается меньше автосборного SIMD-кода.
Кросс-компиляция существующих C / C++ проектов
Многие существующие проекты уже поддерживают SIMD при целевой платформе, в частности, инструкции SSE, AVX на платформах x86 / x86-64 и инструкции NEON на платформах ARM. Обычно они реализуются двумя способами.
Первый способ — через файлы ассемблера, которые выполняют операции SIMD и связываются с кодом C / C++ в процессе сборки. Синтаксис и инструкции ассемблера сильно зависят от платформы и не являются портируемыми, поэтому, чтобы использовать SIMD, такие проекты должны добавить WebAssembly как дополнительную поддерживаемую цель и заново реализовать соответствующие функции, используя либо текстовый формат WebAssembly, либо встроенные функции, описанные выше.
Другой распространённый подход — это использование встроенных функций SSE / SSE2 / AVX / NEON прямо из кода C / C++, где Emscripten может помочь. Emscripten предоставляет совместимые заголовки и слой эмуляции для всех этих наборов инструкций и слой эмуляции, который компилирует их непосредственно в встроенные функции Wasm, если это возможно, или в скалярный код в противном случае.
Чтобы кросс-компилировать такие проекты, сначала включите SIMD через проект-специфические флаги конфигурации, например, ./configure --enable-simd
, чтобы он передал -msse
, -msse2
, -mavx
или -mfpu=neon
компилятору и вызвал соответствующие встроенные функции. Затем дополнительно передайте -msimd128
, чтобы также включить WebAssembly SIMD либо с использованием CFLAGS=-msimd128 make …
/ CXXFLAGS="-msimd128 make …
, либо изменив конфигурацию сборки непосредственно при целевой сборке Wasm.
Построение Rust для целевой SIMD
При компиляции кода Rust для цели WebAssembly SIMD вам необходимо включить ту же функцию LLVM simd128
, что и в Emscripten выше.
Если вы можете управлять флагами rustc
напрямую или через переменную окружения RUSTFLAGS
, передайте -C target-feature=+simd128
:
rustc … -C target-feature=+simd128 -o out.wasm
или
RUSTFLAGS="-C target-feature=+simd128" cargo build
Как и в Clang / Emscripten, автопараллелизация LLVM включена по умолчанию для оптимизированного кода при включенной функции simd128
.
Например, эквивалент кода Rust для приведенного выше примера 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;
});
}
будет производить аналогичный автопараллелизованный код для выровненной части входных данных.
Чтобы получить ручное управление над SIMD-операциями, вы можете использовать ночную версию инструментов, включить функцию Rust wasm_simd
и вызывать встроенные функции из пространства имен std::arch::wasm32
:
#![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);
});
}
Или, вместо этого, используйте вспомогательную библиотеку, такую как packed_simd
, которая абстрагируется от SIMD-реализаций на различных платформах.
Убедительные случаи использования
Предложение WebAssembly SIMD стремится ускорить приложения с высокой вычислительной нагрузкой, такие как аудио/видеокодеки, приложения обработки изображений, криптографические приложения и др. В настоящее время WebAssembly SIMD экспериментально поддерживается в широко используемых проектах с открытым исходным кодом, таких как Halide, OpenCV.js и XNNPACK.
Некоторые интересные демонстрации предоставляет проект MediaPipe от команды Google Research.
Согласно их описанию, MediaPipe — это фреймворк для создания мультимодальных (например, видео, аудио, любые временные ряды данных) прикладных ML-конвейеров. У них также есть веб-версия!
Одной из самых наглядных демонстраций, где легко наблюдать разницу в производительности благодаря SIMD, является версия системы отслеживания рук, работающей только на центральном процессоре (без использования GPU). Без SIMD, вы можете получить только около 14-15 кадров в секунду (FPS) на современном ноутбуке, тогда как с SIMD включенным в Chrome Canary вы получите гораздо более плавный опыт с 38-40 FPS.
Еще одна интересная серия демонстраций, использующих SIMD для создания плавного опыта, поступает из OpenCV — популярной библиотеки компьютерного зрения, которая также может быть скомпилирована в WebAssembly. Они доступны по ссылке, или вы можете посмотреть предподготовленные версии ниже:
Будущая работа
Текущее предложение фиксированной ширины SIMD находится на Фазе 4, поэтому оно считается завершенным.
Уже начались эксперименты с будущими расширениями SIMD, такими как предложение Relaxed SIMD и Flexible Vectors, которые, на момент написания, находятся на Фазе 1.