深入浅出WebAssembly(1) Compilation

5,535 阅读5分钟

这系列主要是我对WASM研究的笔记,可能内容比较简略。总共包括:

  1. 深入浅出WebAssembly(1) Compilation
  2. 深入浅出WebAssembly(2) Basic Api
  3. 深入浅出WebAssembly(3) Instructions
  4. 深入浅出WebAssembly(4) Validation
  5. 深入浅出WebAssembly(5) Memory
  6. 深入浅出WebAssembly(6) Binary Format
  7. 深入浅出WebAssembly(7) Future
  8. 深入浅出WebAssembly(8) Wasm in Rust(TODO)

JS是如何解析运行的?

词法分析

JS代码首先需要经过词法分析器(Lexer)来生成Token,如a = 1 + 2将被解析成{a, =, 1, +, 2}五个Token

语法分析

然后通过语法分析器(Parser)来生成AST,如:对于产生式E -> dpd,我们可以把 S -> veE推导成S -> vedpd

V8引擎架构

  1. Parser: 词法分析 -> 语法分析 -> 语义分析(判断函数参数调用是否正确等等)
  2. Ignition解释器: 基于AST生成 Bytecode
  3. TurboFan优化器: 对代码进行有优化,如果优化代码不可用则进行去优化(Deoptimize)

强弱类型

  1. 弱类型:类型在运行时推断(如js),一般通过JIT技术来提高运行效率。
  2. 强类型:无须推断,可以进行AOT优化,提前编译成二进制机器码。

js引擎中的JIT优化

JIT(just-in-time)指的是在代码动态编译的过程中对一些hot path生成的二进制码进行缓存以提高效率。动态类型的语言因为太过自由,所以某些写法将无法进行JIT优化。 下面这个例子可以很好的对比JIT优化的效果:其中arr1会进行JIT优化,arr2则会频繁去优化: JIT - CodeSandbox

JS能否AOT?

可以,通过ASM.js。例如:

var asm = (function(stdlib, foreign, heap) {
  'use asm';
  function __z3addii($0, $1) {
    $0 = $0|0; // $0 为整型
    $1 = $1|0; // $1 为整型
    var $2 = 0, label = 0, sp = 0;
    $2 = (($1) + ($0))|0;
    return ($2|0); // 返回值被声明为整型
  }
  return {
    add: __z3addii
  }
})(window, null, new ArrayBuffer(0x10000));

需要注意的是:

  1. Annotation: x|0 [32整型], +x [双精度浮点],(x) [单精度浮点] 只能标记数值
  2. stdlib: 一个包含js内置标准库的引用对象,一般可以传 window
  3. foreign: 外部自定义js方法的引用。
  4. heap: 所有asm模块外部数据都要存放在这里面才能被asm模块读取

ASM.js完全兼容js,可以优雅降级。但是缺乏64位整型,只能标注数值类型,编写起来太过困难。因此最终没有大规模普及。

WASM编译基础:LLVM

llvm(low level virtual machine) 通用编译器基础设施,可以用它来开发编译器的前端和后端。目前支持了ActionScript、Ada、D语言、Fortran、GLSL、Haskell、Java字节码、Objective-C、Swift、Python、Ruby、Rust、Scala以及C#等语言

  1. 前端:将程序设计语言转化成中间形式IR (C/C++,Haskell, rust, Swift等)
  2. 后端:指令集的支持(ARM、Qualcomm Hexagon、MIPS等)

IR(intermediate Representation)

LLVM的核心是中间端表达式(Intermediate Representation,IR),一种类似汇编的底层语言。IR是一种强类型的精简指令集(Reduced Instruction Set Computing,RISC),并对目标指令集进行了抽象。例如,目标指令集的函数调用惯例被抽象为call和ret指令加上明确的参数。另外,IR采用无限个数的暂存器,使用如%0,%1等形式表达。LLVM支持三种表达形式:

  1. 人类可读的汇编(*.ll)
  2. 在C++中对象形式
  3. 序列化后的bitcode形式(*.bc)

https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2020/1/15/16fa8e14891543dc~tplv-t2oaga2asx-image.image

  1. 前端(Frontend),负责把各种类型的源代码编译为中间表示,也就是bitcode,在LLVM体系内,不同的语言有不同的编译器前端,最常见的如clang负责c/c++/oc的编译,flang负责fortran的编译,swiftc负责swift的编译等等
  2. 优化(Optimizer),负责对bitcode进行各种类型的优化,将bitcode代码进行一些逻辑等价的转换,使得代码的执行效率更高,体积更小,比如DeadStrip/SimplifyCFG
  3. 后端(Backend),也叫CodeGenerator,负责把优化后的bitcode编译为指定目标架构的机器码,比如X86Backend负责把bitcode编译为x86指令集的机器码
  4. 链接器(Linker): 链接资源(动态,静态)

Bitcode

00000000  de c0 17 0b 00 00 00 00  14 00 00 00 08 0b 00 00  |................|
00000010  07 00 00 01 42 43 c0 de  35 14 00 00 07 00 00 00  |....BC..5.......|
00000020  62 0c 30 24 96 96 a6 a5  f7 d7 7f 4d d3 b4 5f d7  |b.0$.......M.._.|
00000030  3e 9e fb f9 4f 0b 51 80  4c 01 00 00 21 0c 00 00  |>...O.Q.L...!...|
00000040  74 02 00 00 0b 02 21 00  02 00 00 00 13 00 00 00  |t.....!.........|
00000050  07 81 23 91 41 c8 04 49  06 10 32 39 92 01 84 0c  |..#.A..I..29....|
00000060  25 05 08 19 1e 04 8b 62  80 10 45 02 42 92 0b 42  |%......b..E.B..B|
00000070  84 10 32 14 38 08 18 4b  0a 32 42 88 48 90 14 20  |..2.8..K.2B.H.. |
00000080  43 46 88 a5 00 19 32 42  04 49 0e 90 11 22 c4 50  |CF....2B.I...".P|
00000090  41 51 81 8c e1 83 e5 8a  04 21 46 06 51 18 00 00  |AQ.......!F.Q...|

Assembly Language

; ModuleID = 'test.c'
source_filename = "test.c"
target datalayout = "e-m:o-i64:64-f80:128-n8:16:32:64-S128"
target triple = "x86_64-apple-macosx10.14.0"

@.str = private unnamed_addr constant [15 x i8] c"hello, world.\\0A\\00", align 1

; Function Attrs: noinline nounwind optnone ssp uwtable
define i32 @main() #0 {
  %1 = alloca i32, align 4
  store i32 0, i32* %1, align 4
  %2 = call i32 (i8*, ...) @printf(i8* getelementptr inbounds ([15 x i8], [15 x i8]* @.str, i32 0, i32 0))
  ret i32 0
}

declare i32 @printf(i8*, ...) #1

attributes #0 = { noinline nounwind optnone ssp uwtable "correctly-rounded-divide-sqrt-fp-math"="false" "disable-tail-calls"="false" "less-precise-fpmad"="false" "no-frame-pointer-elim"="true" "no-frame-pointer-elim-non-leaf" "no-infs-fp-math"="false" "no-jump-tables"="false" "no-nans-fp-math"="false" "no-signed-zeros-fp-math"="false" "no-trapping-math"="false" "stack-protector-buffer-size"="8" "target-cpu"="penryn" "target-features"="+cx16,+fxsr,+mmx,+sahf,+sse,+sse2,+sse3,+sse4.1,+ssse3,+x87" "unsafe-fp-math"="false" "use-soft-float"="false" }
attributes #1 = { "correctly-rounded-divide-sqrt-fp-math"="false" "disable-tail-calls"="false" "less-precise-fpmad"="false" "no-frame-pointer-elim"="true" "no-frame-pointer-elim-non-leaf" "no-infs-fp-math"="false" "no-nans-fp-math"="false" "no-signed-zeros-fp-math"="false" "no-trapping-math"="false" "stack-protector-buffer-size"="8" "target-cpu"="penryn" "target-features"="+cx16,+fxsr,+mmx,+sahf,+sse,+sse2,+sse3,+sse4.1,+ssse3,+x87" "unsafe-fp-math"="false" "use-soft-float"="false" }

!llvm.module.flags = !{!0, !1}
!llvm.ident = !{!2}

!0 = !{i32 1, !"wchar_size", i32 4}
!1 = !{i32 7, !"PIC Level", i32 2}
!2 = !{!"Apple LLVM version 10.0.0 (clang-1000.11.45.5)"}

模块(Module),函数(Function),代码块(BasicBlock),指令(Instruction) 模块包含了函数,函数又包含了代码块,后者又是由指令组成。除了模块以外,所有结构都是从值产生而来的。

WASM编译基础:Emscripten

asm标准严格,书写不便,可以通过Emscripten来将C/C++代码转译成asmjs。 Empscripten是基于的llvm工具链,由两部分构成:

  1. 前端(emcc,Emscripten Compiler Frontend): 将C/C++编译成LLVM的IR, why clang ? 因为emcc将会提供一些独特的功能比如说宏定义等
  2. 后端(Fastcomp) 将LLVM中间代码编译到目标语言(js)

WASM如何编译?

C/C++编译到WASM的方式:

  1. 远古方法:利用s2wasm(已弃用)将assembly转化成webAssembly。C/C++ - {Clang} -> LLVM IR - {llc} -> *.s - {s2wasm} -> wasm
  2. 传统方法: 先编译到ASM.js再通过asm2wasm转化成webAssembly。C/C++ - {emcc/Fastcomp} -> ASM.js - {asm2wasm} -> wasm
  3. 现代方法(2019.3+): 直接通过LLVM 8编译成webAssembly。C/C++ - {Clang} -> LLVM IR - {llc && wasmld} -> wasm

LLVM 8的编译细节

LLVM 8提供了完整的wasm后端支持,可以直接将LLVM IR 转化成wasm。 (target --wasm), 以为所有能够编译到llvm IR的语言都能很方便的转化为wasm

target=wasm?

llvm相关wasm文档: Instructions — WebAssembly 1.0WebAssembly lld port — lld 10 documentationWebAssembly and Dynamic Memory | Frank Rehberger

libc or libc++?

如何在C/C++里面使用malloc?new? algorithm?

  1. emcc(dlmalloc): emscripten/library_syscall.js at incoming · emscripten-core/emscripten · GitHub
  2. wasmception(musl-libc): GitHub - yurydelendik/wasmception: Minimal C/C++ language toolset for building wasm files demo: WebAssembly Studio
  3. wasi(dlmalloc): GitHub - CraneStation/wasi-libc: WASI libc implementation for WebAssembly

其他语言的编译工具:

Binaryen

webassembly官方的套工具链,提供了一套binaryen IR与后端, 目前比较著名的前端有:

  1. AssemblyScript: Typescript
  2. Asterius: Haskell
  3. asm2wasm: asm.js

rustc

rust的官方编译器,已经可以直接支持target=wasm32-unknown-unknown。值得一题的是,rustc目前拥有一个十分优秀的wasm内存分配库wee_alloc,可以直接生成相关wasm内存指令而不需要通过胶水代码:

https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2020/1/15/16fa8eb859542d28~tplv-t2oaga2asx-image.image