「这是我参与11月更文挑战的第28天,活动详情查看:2021最后一次更文挑战」
SIMD(Single Instruction Multiple Data)中文为单指令多数据,它是一种特的 CPU 指令,能够实现数据的并行处理,在音视频和图像处理领域有广泛的使用。与之相反的概念是 SISD (Single Instruction Single Data),每次数据计算都需要执行一次指令。随着浏览器能力的不断增强,现在有越来越多的基于 Web 技术的多媒体应用,本文看一看 SIMD 技术在前端的发展和应用。
先说结论:JavaScript 不支持 SIMD 且大概率不会考虑支持 SIMD,WebAssembly 正在逐步支持 SIMD,其中 Chrome 高于 91 版本默认开启 SIMD。
在 Javascript 中曾经有过 SIMD 的相关提案,当时一段 SIMD 计算的程序大概是这样的:
var a = SIMD.Float32x4(1, 2, 3, 4);
var b = SIMD.Float32x4(5, 6, 7, 8);
var c = SIMD.Float32x4.add(a,b); // Float32x4[6,8,10,12]
之前需要四条指令才能完成的计算,使用 SIMD 计算只需要一条指令即可完成:
遗憾的是现在这段程序没办法在任何浏览器中运行了,SIMD 的提案在 Stage 3 阶段被移除了,浏览器不会实现 JavaScript 的 SIMD API,Web 端的 SIMD 能力将通过 WebAssembly 来支持。之前留下的相关 TC39 工作记录可以在 github 上查看到,现在研究 JavaScript 的 SIMD 已经没有实际意义了。
WebAssembly 中 SIMD 的实现已经比较成熟了,目前处于 Phase 4 阶段,相关的规范也遵从之前 TC39 的内容,由于只在新浏览器中支持,使用时需要构建带 SIMD 和不带 SIMD 两个版本的代码,运行时进行判断和加载:
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
})();
使用 emscripten SDK 添加 -msimd128 参数可以编译带 SIMD 的 WebAssembly,在编译时会基于 LLVM 的自动向量优化机制,对于可以合并的数据指令进行合并,充分发挥 SIMD 的优势。下面是一段普通的 C 程序,实现两个数组乘法,在图形处理中经常有向量和矩阵的操作,这种计算很常见:
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];
}
}
不使用 SIMD 时,生成的 wasm 资源是像下面这样的,就是一个普通的循环操作,每次 load 一组数据进行计算:
(loop
(i32.store
… get address in `out` …
(i32.mul
(i32.load … get address in `in_a` …)
(i32.load … get address in `in_b` …)
…
)
添加 SIMD 参数后构建出来的 wasm 是这样的,每次会去读取四个元素,一条指令就可以完成四组数据的计算过程,可以很大程度提升效率:
(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` …)
…
)
)
在 C 程序中,也可以引入 <wasm_simd128.h> 使用内部提供的 SIMD 相关数据类型,手动控制 SIMD 的计算。这样的控制会增加开发成本,但是比自动向量更精确,可以更灵活更节约空间。
在 Rust 中也可以为 rustc 添加参数 -C target-feature=+simd128 来开启 SIMD 编译,同样可以支持自动向量的优化。