浏览器中的 SIMD

1,977 阅读2分钟

「这是我参与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 计算只需要一条指令即可完成:

image.png

遗憾的是现在这段程序没办法在任何浏览器中运行了,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 编译,同样可以支持自动向量的优化。

参考:v8.dev/features/si…