SIMD 代表 *单指令,多数据*。SIMD 指令是一种特殊的指令类别,通过对多个数据元素同时执行相同的操作来利用应用程序中的数据并行性。计算密集型应用程序,如音频/视频编解码器、图像处理器,都是利用 SIMD 指令来加速性能的应用程序示例。大多数现代架构都支持 SIMD 指令的某些变体。
WebAssembly SIMD 提案定义了可移植的、高性能的 SIMD 操作子集,这些操作子集在大多数现代架构中都可用。该提案借鉴了 SIMD.js 提案 的许多元素,而 SIMD.js 提案最初又是从 Dart SIMD 规范中衍生出来的。SIMD.js 提案是在 TC39 中提出的一个 API,它包含用于执行 SIMD 计算的新类型和函数,但该提案已被存档,转而支持在 WebAssembly 中更透明地支持 SIMD 操作。 WebAssembly SIMD 提案 作为一种方法被引入,以便浏览器利用底层硬件的数据级并行性。
WebAssembly SIMD 提案 #
WebAssembly SIMD 提案的高级目标是将向量操作引入 WebAssembly 规范,以一种保证可移植性能的方式。
SIMD 指令集很大,并且在不同架构之间存在差异。WebAssembly SIMD 提案中包含的操作集包括在各种平台上得到良好支持的操作,并且已被证明具有高性能。为此,当前提案仅限于标准化固定宽度 128 位 SIMD 操作。
当前提案引入了一种新的 v128
值类型,以及许多对该类型进行操作的新操作。用于确定这些操作的标准是
- 这些操作应该在多个现代架构中得到良好支持。
- 性能提升应该在指令组内的多个相关架构中都是积极的。
- 选择的操作集应该最大限度地减少性能断崖(如果有)。
该提案现在处于 最终状态(第 4 阶段),V8 和工具链都有工作实现。
启用 SIMD 支持 #
功能检测 #
首先,请注意 SIMD 是一项新功能,尚未在所有支持 WebAssembly 的浏览器中可用。您可以在 webassembly.org 网站上找到哪些浏览器支持新的 WebAssembly 功能。
为了确保所有用户都能加载您的应用程序,您需要构建两个不同的版本 - 一个启用 SIMD,另一个不启用 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')
);
// …now use `module` as you normally would
})();
要了解有关使用 SIMD 支持构建代码的信息,请查看 下面的部分。
浏览器中的 SIMD 支持 #
从 Chrome 91 开始,WebAssembly SIMD 支持默认可用。请确保使用下面详细说明的最新版本的工具链,以及最新的 wasm-feature-detect 来检测支持规范最终版本的引擎。如果某些内容看起来不对劲,请 提交错误报告。
Firefox 89 及更高版本也支持 WebAssembly SIMD。
使用 SIMD 支持构建 #
将 C / C++ 构建为目标 SIMD #
WebAssembly 的 SIMD 支持依赖于使用最新版本的 clang,并启用 WebAssembly LLVM 后端。Emscripten 也支持 WebAssembly SIMD 提案。使用 emsdk 安装并激活 emscripten 的 latest
发行版,以使用 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 指令一次对多个输入执行相同的算术运算。当提供 -msimd128
标志时,LLVM 的自动矢量化器在优化级别 -O2
和 -O3
时默认启用。
例如,考虑以下函数,它将两个输入数组的元素相乘并将结果存储在输出数组中。
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
… get address in `out` …
(i32.mul
(i32.load … get address in `in_a` …)
(i32.load … get address in `in_b` …)
…
)
但是,当使用 -msimd128
标志时,自动矢量化器会将其转换为包含以下循环的代码
(loop
(v128.store align=4
… get address in `out` …
(i32x4.mul
(v128.load align=4 … get address in `in_a` …)
(v128.load align=4 … get address in `in_b` …)
…
)
)
循环体具有相同的结构,但 SIMD 指令被用于在循环体中一次加载、乘以和存储四个元素。
为了更精细地控制编译器生成的 SIMD 指令,请包含 wasm_simd128.h
头文件,该文件定义了一组内联函数。内联函数是特殊的函数,当调用时,编译器会将其转换为相应的 WebAssembly SIMD 指令,除非它可以进行进一步的优化。
例如,以下是之前相同函数的手动重写版本,它使用 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);
}
}
此手动重写的代码假设输入和输出数组是已对齐的,并且没有别名,并且大小是 4 的倍数。自动矢量化器无法做出这些假设,并且必须生成额外的代码来处理不满足这些假设的情况,因此手写的 SIMD 代码通常比自动矢量化的 SIMD 代码更小。
交叉编译现有的 C / C++ 项目 #
许多现有项目在针对其他平台时已经支持 SIMD,特别是 x86 / x86-64 平台上的 SSE 和 AVX 指令,以及 ARM 平台上的 NEON 指令。通常有两种方法来实现这些指令。
第一种方法是通过汇编文件来处理 SIMD 操作,并在构建过程中与 C / C++ 链接在一起。汇编语法和指令高度依赖于平台,并且不可移植,因此,为了利用 SIMD,此类项目需要添加 WebAssembly 作为另一个支持的目标,并使用 WebAssembly 文本格式 或上面描述的内联函数重新实现相应的函数。
另一种常见的方法是直接从 C / C++ 代码中使用 SSE / SSE2 / AVX / NEON 内联函数,而 Emscripten 可以提供帮助。Emscripten 提供兼容的头文件和模拟层,用于所有这些指令集,以及一个模拟层,该模拟层将它们直接编译为 Wasm 内联函数(如果可能),或者在其他情况下编译为标量代码。
要交叉编译此类项目,首先通过项目特定的配置标志启用 SIMD,例如 ./configure --enable-simd
,以便它将 -msse
、-msse2
、-mavx
或 -mfpu=neon
传递给编译器并调用相应的内联函数。然后,通过使用 CFLAGS=-msimd128 make …
/ CXXFLAGS="-msimd128 make …
或在针对 Wasm 时直接修改构建配置,额外传递 -msimd128
以启用 WebAssembly SIMD。
将 Rust 构建为目标 SIMD #
将 Rust 代码编译为目标 WebAssembly SIMD 时,您需要启用与上面 Emscripten 中相同的 simd128
LLVM 功能。
如果您可以直接控制 rustc
标志或通过环境变量 RUSTFLAGS
控制,请传递 -C target-feature=+simd128
rustc … -C target-feature=+simd128 -o out.wasm
或者
RUSTFLAGS="-C target-feature=+simd128" cargo build
与 Clang / Emscripten 一样,当启用 simd128
功能时,LLVM 的自动矢量化器默认情况下对优化代码启用。
例如,上面 multiply_arrays
示例的 Rust 等效项
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 操作,您可以使用 nightly 工具链,启用 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。
一些有趣的演示来自 Google Research 团队的 MediaPipe 项目。
根据他们的描述,MediaPipe 是一个用于构建多模态(例如视频、音频、任何时间序列数据)应用 ML 管道的框架。他们也有一个 Web 版本!
最具视觉吸引力的演示之一是手部跟踪系统的纯 CPU(非 GPU)构建,在该演示中,很容易观察到 SIMD 带来的性能差异。 没有 SIMD 时,您只能在现代笔记本电脑上获得大约 14-15 FPS(每秒帧数),而 在 Chrome Canary 中启用 SIMD 后,您可以在 38-40 FPS 下获得更流畅的体验。
另一组有趣的演示利用 SIMD 来实现流畅的体验,这些演示来自 OpenCV - 一个流行的计算机视觉库,它也可以编译为 WebAssembly。您可以通过 链接 获取这些演示,或者查看下面的预录制版本
未来工作 #
当前的固定宽度 SIMD 提案处于 第 4 阶段,因此它被认为是完整的。
一些关于未来 SIMD 扩展的探索已经开始在 Relaxed SIMD 和 Flexible Vectors 提案中进行,截至撰写本文时,这些提案处于第 1 阶段。