WebGL Tutorial
and more

Vector Instructions

撰写时间:2025-02-09

修订时间:2025-02-16

概述

vector是一种向量操作指令,属于SIMD指令。

SIMD (Single instruction, multiple data) 是一种并行计算技术,允许一条指令同时操作多个数据,从而显著提升数据密集型任务的执行效率。其核心思想是通过单条指令并行处理多个数据,适用于高度规则且数据独立的任务。

SIMD是指令级并行,由硬件直接支持,集成在通用CPU中,具有灵活性及通用性。而多线程(如多核CPU)是线程级并行,依赖于操作系统的调度。两者结合使用,能实现更高呑吐。

SIMD可应用于:

  • 图像处理:使用SIMD加速像素操作(如对RGBA通道进行分离、缩放、或应用滤镜)。
  • 机器学习:矩阵乘法和激活函数的并行计算。
  • 游戏开发:物理引擎中的碰撞检测、粒子系统运算。
  • 个人知识库的建立:提取关键字,并进行向量化。

综上,SIMD通过单指令操作多数据,在多媒体处理、科学计算等领域至关重要。结合现代CPUSIMD扩展指令集,开发者能显著提升程序性能。

因此,vectorWasm给开发者带来的一份厚礼。

通过调用storeload指令,vector也可用于内存操作。

设置常数

const { getHexAddrStrFromTypedArray } = await import('/js/esm/BinUtils.js'); let watSrc = ` (module (memory (export "memory") 1) (func (v128.store (i32.const 0) (v128.const i32x4 1 2 3 4)) (v128.store (i32.const 16) (v128.const i64x2 5 6)) ) (start 0) ) `; const { memory } = await WabtUtils.RunWat(watSrc); let tarr = new Uint8Array(memory.buffer, 0, 64); pc.log('%s', getHexAddrStrFromTypedArray(tarr, 16));

代码:

v128.const i32x4 1 2 3 4

4i32的值分别设置为1, 2, 3, 4。其好处是在128位(16个字节)的范围内,这4个数值自动按i32的位域对齐,也即各个数值的内存区域不会相互覆盖。

指令含义
v128.const i8x16 (n:i8)16同时生成字长均为1字节的16个整数常数
v128.const i16x8 (n:i16)8同时生成字长均为2字节的8个整数常数
v128.const i32x4 (n:i32)4同时生成字长均为4字节的4个整数常数
v128.const i64x2 (n:i64)2同时生成字长均为8字节的2个整数常数
v128.const f32x4 (n:f32)4同时生成字长均为4字节的4个浮点常数
v128.const f64x2 (n:f64)2同时生成字长均为8字节的2个浮点常数

示例代码如下:

const { getHexAddrStrFromTypedArray } = await import('/js/esm/BinUtils.js'); let watSrc = ` (module (memory (export "memory") 1) (func (v128.store (i32.const 0) (v128.const i8x16 0x00 0x01 0x02 0x03 0x04 0x05 0x06 0x07 0x08 0x09 0x0A 0x0B 0x0C 0x0D 0x0E 0x0F)) (v128.store (i32.const 16) (v128.const i16x8 1 2 3 4 5 6 7 8)) (v128.store (i32.const 32) (v128.const i32x4 1 2 3 4)) (v128.store (i32.const 48) (v128.const i64x2 1 2)) (v128.store (i32.const 64) (v128.const f32x4 1.5 2.5 3.5 4.5)) (v128.store (i32.const 80) (v128.const f64x2 1.5 2.5)) ) (start 0) ) `; const { memory } = await WabtUtils.RunWat(watSrc); let tarr = new Uint8Array(memory.buffer, 0, 16 * 6); pc.log('%s', getHexAddrStrFromTypedArray(tarr, 16));

store

store指令用于在内存区域中存储数值。

基本用法

const { getHexAddrStrFromTypedArray } = await import('/js/esm/BinUtils.js'); let watSrc = ` (module (memory (export "memory") 1) (func (v128.store (i32.const 0) (v128.const i32x4 1 2 3 4)) ) (start 0) ) `; const { memory } = await WabtUtils.RunWat(watSrc); let tarr = new Uint8Array(memory.buffer, 0, 32); pc.log('%s', getHexAddrStrFromTypedArray(tarr, 16));

在默认内存区域偏移值为0的地方,存储了指定的4i32的整数数值。

选取特定通道的值进行存储

可以从要存储的一系列数值中,按不同的位域分成不同的组,再取出特定组别中的数值,再予以存储。

const { getHexAddrStrFromTypedArray } = await import('/js/esm/BinUtils.js'); let watSrc = ` (module (memory (export "memory") 1) (func (v128.store8_lane 7 (i32.const 0) (v128.const i8x16 0x00 0x01 0x02 0x03 0x04 0x05 0x06 0x07 0x08 0x09 0x0A 0x0B 0x0C 0x0D 0x0E 0x0F)) ) (start 0) ) `; const { memory } = await WabtUtils.RunWat(watSrc); let tarr = new Uint8Array(memory.buffer, 0, 32); pc.log('%s', getHexAddrStrFromTypedArray(tarr, 16));

代码从v128.const i8x16所表示的16个各为1个字节的整数数组中,取出位于第7通道lane)的值,保存在偏移值为0的内存区域中。

lane就是在常数数组上所建立起来的通道。对于store8_lane,先按8位即1字节为单位来划分为16个通道,每个通道1字节。

然后,代码:

v128.store8_lane 7 ...

取出第7个通道的数值,其值为0x07,存储到内存区域中。

划分通道时不受设置常数时所使用的操作数的数据类型的约束。下面代码,按i16来设置一组常数,但按8位来划分通道。

const { getHexAddrStrFromTypedArray } = await import('/js/esm/BinUtils.js'); let watSrc = ` (module (memory (export "memory") 1) (func (v128.store (i32.const 0) (v128.const i16x8 0x11 0x22 0x33 0x44 0x55 0x66 0x77 0x88)) (v128.store8_lane 0x0A (i32.const 16) (v128.const i16x8 0x11 0x22 0x33 0x44 0x55 0x66 0x77 0x88)) ) (start 0) ) `; const { memory } = await WabtUtils.RunWat(watSrc); let tarr = new Uint8Array(memory.buffer, 0, 32); pc.log('%s', getHexAddrStrFromTypedArray(tarr, 16));

上面两行代码,均使用相同的数据类型为i16、数量为8个的整数常数数组,分别填充了内存区域的第1行及第2行。而在填充第2行时,先按字节划分通道,然后只取出第0x0A个字节处的数值66予以填充。

下面代码从要存储的一系列数值中,分别按8位、16位、32位、64位来划分通道后,均取出第1个通道的值存储进内存中。

const { getHexAddrStrFromTypedArray } = await import('/js/esm/BinUtils.js'); let watSrc = ` (module (memory (export "memory") 1) (func (v128.store (i32.const 0) (v128.const i8x16 0x00 0x01 0x02 0x03 0x04 0x05 0x06 0x07 0x08 0x09 0x0A 0x0B 0x0C 0x0D 0x0E 0x0F)) (v128.store8_lane 1 (i32.const 16) (v128.const i8x16 0x00 0x01 0x02 0x03 0x04 0x05 0x06 0x07 0x08 0x09 0x0A 0x0B 0x0C 0x0D 0x0E 0x0F)) (v128.store16_lane 1 (i32.const 32) (v128.const i8x16 0x00 0x01 0x02 0x03 0x04 0x05 0x06 0x07 0x08 0x09 0x0A 0x0B 0x0C 0x0D 0x0E 0x0F)) (v128.store32_lane 1 (i32.const 48) (v128.const i8x16 0x00 0x01 0x02 0x03 0x04 0x05 0x06 0x07 0x08 0x09 0x0A 0x0B 0x0C 0x0D 0x0E 0x0F)) (v128.store64_lane 1 (i32.const 64) (v128.const i8x16 0x00 0x01 0x02 0x03 0x04 0x05 0x06 0x07 0x08 0x09 0x0A 0x0B 0x0C 0x0D 0x0E 0x0F)) ) (start 0) ) `; const { memory } = await WabtUtils.RunWat(watSrc); let tarr = new Uint8Array(memory.buffer, 0, 16 * 5); pc.log('%s', getHexAddrStrFromTypedArray(tarr, 16));

v128的字长为16个字节,因此,通道指令的本质就是从字长为16个字节的一组数据中,划分为不同字长的通道。

下表列出存储数值时可划分通道的所有指令。

v128.store<n>_lane指令表
指令通道字长通道数量能选取的通道范围选取数值的字长
v128.store8_lane1字节16[0, 15]1字节
v128.store16_lane2字节8[0, 7]2字节
v128.store32_lane4字节4[0, 3]4字节
v128.store64_lane8字节2[0, 1]8字节

load

v128load指令用于从内存区域中读取数据,并返回一个v128的值。

v128返回值的问题

Wasm内部,函数可以返回v128的值,但由于JavaScript不直接支持该数据类型,因此,需返回v128的值的函数不能导出至JavaScript中调用。下面的代码将抛出异常。

const { getHexAddrStrFromTypedArray, getHexStr } = await import('/js/esm/BinUtils.js'); let watSrc = ` (module (memory (export "memory") 1) (func $store (v128.store (i32.const 0) (v128.const i32x4 1 2 3 4)) ) (func (export "load") (result v128) (v128.load (i32.const 0)) ) (start $store) ) `; const { memory, load} = await WabtUtils.RunWat(watSrc); let tarr = new Uint8Array(memory.buffer, 0, 16); let str = getHexAddrStrFromTypedArray(tarr, 16); pc.log('%s', str); load();

在Safari中,异常信息为:Exception thrown: an exported wasm function cannot contain a v128 parameter or return value

在Chrome中,异常信息为:Exception thrown: type incompatibility when transforming from/to JS

此时,可换个思路,不调用v128.load指令,而是改为调用v128.store指令来改写内存区域的内容,再将内存区域导出至JavaScript中操作。

在Wasm内部调用load指令

load指令将指定偏移值的向量读取后,再压进Result Stack中,利用此特性,我们可以在Result Stack中进行相应的运算。

const { getHexAddrStrFromTypedArray, getHexStr } = await import('/js/esm/BinUtils.js'); let watSrc = ` (module (memory (export "memory") 1) (func $store (v128.store (i32.const 0) (v128.const i32x4 1 2 3 4)) ) (func (export "doubleUp") i32.const 16 (i32x4.mul (v128.load (i32.const 0)) (v128.const i32x4 2 2 2 2) ) v128.store ) (start $store) ) `; const { memory, doubleUp} = await WabtUtils.RunWat(watSrc); doubleUp(); let tarr = new Uint8Array(memory.buffer, 0, 32); let str = getHexAddrStrFromTypedArray(tarr, 16); pc.log('%s', str);

程序一开始,先在内存区域 [0x00, 0x0F] 的位置存储了432位的数值。而在doubleUp函数中,代码:

(i32x4.mul (v128.load (i32.const 0)) (v128.const i32x4 2 2 2 2) )

调用v128.load指令,从第0个字节处读取432位的数值,压进Result Stack中,然后将4个值均为2的常数压进栈,再将它们按各自的通道分别相乘。

之后,调用v128.store,将结果存储到内存区域偏移值为16的地方。最后,打印内存区域。

由上例可见,v128.load所返回的结果,可用作诸如i32x4.mulSIMD指令的参数。

v128.load8x8_sx指令

我们准备选用下面一组数据。

const { getBinStr, getHexStr } = await import('/js/esm/BinUtils.js'); let highPartStr = '1000_0000'; let lowPartStr = incr(highPartStr); for (let i = 0; i < 8; i++) { let binStr = highPartStr + ' ' + lowPartStr; binStr = normalizeBinStr(binStr); let num = parseInt(binStr, 2); pc.log('%s <===> %s', getHexStr(num), getBinStr(num)); highPartStr = incr(lowPartStr); lowPartStr = incr(highPartStr); } function incr(binStr) { binStr = normalizeBinStr(binStr); return (parseInt(binStr, 2) + 1).toString(2); } function normalizeBinStr(binStr) { return binStr.replaceAll(/0B|0b|_| /g, ''); }

这组数据有以下特点:

  1. 字长均为2字节
  2. 高字节的值比低字节的值小1
  3. 8位及后8位,其最高权重位均为1,可代表符号位
  4. 每个字节的十六进制均可独立辨识
  5. 每个数值均比上个数值大2

下面在Wasm中使用这组数据。

const { getHexAddrStrFromTypedArray, getBinStr, getHexStr } = await import('/js/esm/BinUtils.js'); let watSrc = ` (module (memory (export "memory") 1) (func $init (v128.store (i32.const 0) (v128.const i16x8 0x8081 0x8283 0x8485 0x8687 0x8899 0x8A8B 0x8C8D 0x8E8F)) ) (start $init) (func (export "load8x8") (v128.store (i32.const 16) (v128.load8x8_u (i32.const 0)) ) (v128.store (i32.const 32) (v128.load8x8_s (i32.const 8)) ) ) ) `; const { memory, load8x8} = await WabtUtils.RunWat(watSrc); load8x8(); let tarr = new Uint8Array(memory.buffer, 0, 48); let str = getHexAddrStrFromTypedArray(tarr, 16); pc.log('%s', str);

在内存示意图中,第一行是内存原始数据;第二行是调用v128.load8x8_u指令加载相应数据后再存储回内存中的数据;第三行是调用v128.load8x8_s指令加载相应数据后再存储回内存中的数据。

v128.load8x8_sx连续加载位域为8位共计8个的数值,并将每个数值都扩展16位的数值,最终将这些扩展后的数值组合成一个128位的SIMD向量。需指定高位字节的扩展方式。

v128.load8x8_u指令将每个数值都零扩展zero-extend)为16位,即在扩展后的16位数值的高字节部位以数值0x00填充。

v128.load8x8_s指令将每个数值都符号扩展sign-extend)为16位。符号扩展的规则为:

  • 若原始数值为正数(最高位为0),则在扩展后的高字节部位填充数值0x00
  • 若原始数值为负数(最高位为1),则在扩展后的高字节部位填充数值0xFF,以保持数值的符号和大小不变。

参考资源

Core

  1. Vector Instructions
  2. Sign Interpretation
  3. Feature Extensions
  4. W3C version (single page)
  5. Doc version (webassembly.org)

Gitee

  1. simd test

Github

  1. simd test
  2. WebAssembly Reference Interpreter