X

曜彤.手记

随记,关于互联网技术、产品与创业

  1. Bulk Memory Operations
  2. Conditional Segment Initialization
  3. Instruction Encoding
  4. 其他信息

WebAssembly - Bulk Memory Operations and Conditional Segment Initialization

今天来看的提案是 - “Bulk Memory Operations and Conditional Segment Initialization”,GitHub 链接在这里。该提案提供了更高效的线性内存和 Table 操作指令(类似于某种 “super instruction”),以及针对两者的条件初始化能力。

Bulk Memory Operations

提案的背景源于 MVP 标准下,Wasm 在编译使用了 memcpy 与 memmove 等批量内存操作函数时会采用 i32.loadi32.store 等线性内存指令来模拟实现,而这些指令在每次执行时仅能够操作固定字节宽度的数据,因此,对于内存操作密集型应用来说,这些指令的执行便占用了大量的时钟时间(指令 dispatch 耗费了大量时间)。比如下面这条评论:

In a number of game engines I’ve been optimizing and benchmarking, interestingly the performance of memcpy() does show up relatively high in profiles. (~2%-5% of total execution time).

为此,提案新增了指令可用于批量操作线性内存,当然也包括 table。来看下面这个例子:

(module
  (memory (export "memory") 1 1)  ;; Initial -> 1 page, maximum -> 1 page.
  (data (i32.const 0) "\01\02\03\04")
  (func $start
    (memory.copy (i32.const 2) (i32.const 0) (i32.const 2))  ;; Copy memory data.
  )  
  (func (export "load8_u") (param i32) (result i32)
    (i32.load8_u (local.get 0)))
  (start $start)
) 

在这段 WAT 中我们定义了两个函数,其中 “$start” 会在模块实例化时被执行,“$load8_u” 会被导出到宿主环境中使用。代码第二行定义了 Wasm 模块使用的线性内存段;第三行定义的 data 段初始化了线性内存中的数据。在代码第五行的函数 “$start” 内部,我们使用了提案新增加的指令 memory.copy,该指令将以默认内存段偏移 “(i32.const 2)” 个字节的地方作为数据拷贝的源地址,以偏移 “(i32.const 0)” 个字节的地方作为目的地址,拷贝长度为 “(i32.const 2)” 字节的数据。

WebAssembly.instantiate(wasmModule).then(
  (instance) => { 
    const { load8_u } = instance.exports;
    for (let i = 0; i < 4; ++i) {
      console.log(load8_u(i));  // “1 2 1 2”.
    }    
  });           

导出的函数 “$load8_u” 可用于查看默认线性内存指定字节偏移位置处的数据值。通过上述 JavaScript 代码,我们可以使用该 Wasm 模块,并验证指令的执行结果符合我们的预期。

Conditional Segment Initialization

除了对已有线性内存或 table 进行数据拷贝和填充外,对两者的“条件初始化”能力也被考虑了进来。增加这个能力的起因源于“在多个 agent 之间共享 Wasm 模块”的场景。正常情况下,Wasm 模块的线性内存段会在模块实例化时由执行引擎自动完成加载,而当一个 Wasm 模块被多个 agent 同时使用,且它们共享同一块线性内存时,该内存区域便会在不同模块实例化时被多次加载,以至某些来自于前一个模块的计算结果值被覆盖。

“条件初始化”,意味着原有的线性内存和 table 初始化过程都变得可控,而不再局限于仅由 Wasm 执行环境自动完成。看下面这个例子:

(module
  (memory (export "memory") 1 1)
  (data (i32.const 0) "\01\02\03\04")
  (data "\05\06\07\08")  ;; Define a "passive" data segment.
  (func
    (memory.init 1 (i32.const 2) (i32.const 2) (i32.const 2))  ;; Initialize memory with given segment.
    (data.drop 1)
  )  
  (func (export "load8_u") (param i32) (result i32)
    (i32.load8_u (local.get 0)))
  (start 0)
) 

WAT 的大部分内容与上一小节中的例子类似,这里我们在代码第四行又定义了一个 data 段结构,这个段结构由于没有指定初始化偏移位置,因为它被默认定义为 “passive segment”,而对于这种段结构,它不会被自动填充到对应的 memory 或 table 结构中,相反则需要通过显式指令完成这个过程。这个指令就是代码第六行出现的 memory.init,该指令可以将给定的 data 段结构数据填充到指定线性内存段的某个位置。需要注意的是,active 类型的 data 或 element 段会在执行引擎完成数据填充后被自动“丢弃”,正如我们在代码第七行通过指令 data.drop 所做的事一样。被丢弃后的段可用数据长度为 0。

WebAssembly.instantiate(wasmModule).then((instance) => { 
  const { load8_u } = instance.exports;
  for (let i = 0; i < 4; ++i) {
    console.log(load8_u(i));  // “1 2 7 8”.
  }    
});           

Instruction Encoding

提案新增加的所有指令可以参考下表:

Name Opcode Immediate Desc
memory.init 0xfc 0x08 segment:varuint32, memory:0x00 从一个 “passive data” 段拷贝数据到线性内存
data.drop 0xfc 0x09 segment:varuint32 丢弃一个 “passive data” 段
memory.copy 0xfc 0x0a memory_dst:0x00 memory_src:0x00 拷贝内存
memory.fill 0xfc 0x0b memory:0x00 用指定字节填充内存
table.init 0xfc 0x0c segment:varuint32, table:0x00 从一个 “passive element” 段拷贝数据到 table
elem.drop 0xfc 0x0d segment:varuint32 丢弃一个 “passive element” 段
table.copy 0xfc 0x0e table_dst:0x00 table_src:0x00 拷贝 table

其他信息

提案新增加的 “memory.{init,drop}” 指令使得我们可以直接在 Code Section 内访问 Data Section 中的段结构,这打破了 Wasm 设计上的 “single-pass validation”(即编译模块当前位置需要的信息都可以在前面几步中获得)结构。为此,提案新增加了名为 “DataCount” 的可选 Section (code = 12),该结构位于 Code Section 之前,其中存放有当前 Wasm 模块中定义的 data 段数量。当验证时出现以下情况则表示模块结构的异常:

  • DataCount Section 中存放的 count 值不等于 Data Section 中定义的 data 段数量;
  • DataCount Section 未定义,但模块内使用了 memory.initmemory.drop 指令。



评论 | Comments


Loading ...