这系列主要是我对WASM研究的笔记,可能内容比较简略。总共包括:
- 深入浅出WebAssembly(1) Compilation
- 深入浅出WebAssembly(2) Basic Api
- 深入浅出WebAssembly(3) Instructions
- 深入浅出WebAssembly(4) Validation
- 深入浅出WebAssembly(5) Memory
- 深入浅出WebAssembly(6) Binary Format
- 深入浅出WebAssembly(7) Future
- 深入浅出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引擎架构
- Parser: 词法分析 -> 语法分析 -> 语义分析(判断函数参数调用是否正确等等)
- Ignition解释器: 基于AST生成 Bytecode
- TurboFan优化器: 对代码进行有优化,如果优化代码不可用则进行去优化(Deoptimize)
强弱类型
- 弱类型:类型在运行时推断(如js),一般通过JIT技术来提高运行效率。
- 强类型:无须推断,可以进行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));
复制代码
需要注意的是:
- Annotation:
x|0
[32整型],+x
[双精度浮点],(x)
[单精度浮点] 只能标记数值 - stdlib: 一个包含js内置标准库的引用对象,一般可以传
window
- foreign: 外部自定义js方法的引用。
- 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#等语言
- 前端:将程序设计语言转化成中间形式IR (C/C++,Haskell, rust, Swift等)
- 后端:指令集的支持(ARM、Qualcomm Hexagon、MIPS等)
IR(intermediate Representation)
LLVM的核心是中间端表达式(Intermediate Representation,IR),一种类似汇编的底层语言。IR是一种强类型的精简指令集(Reduced Instruction Set Computing,RISC),并对目标指令集进行了抽象。例如,目标指令集的函数调用惯例被抽象为call和ret指令加上明确的参数。另外,IR采用无限个数的暂存器,使用如%0,%1等形式表达。LLVM支持三种表达形式:
- 人类可读的汇编(*.ll)
- 在C++中对象形式
- 序列化后的bitcode形式(*.bc)
- 前端(Frontend),负责把各种类型的源代码编译为中间表示,也就是bitcode,在LLVM体系内,不同的语言有不同的编译器前端,最常见的如clang负责c/c++/oc的编译,flang负责fortran的编译,swiftc负责swift的编译等等
- 优化(Optimizer),负责对bitcode进行各种类型的优化,将bitcode代码进行一些逻辑等价的转换,使得代码的执行效率更高,体积更小,比如DeadStrip/SimplifyCFG
- 后端(Backend),也叫CodeGenerator,负责把优化后的bitcode编译为指定目标架构的机器码,比如X86Backend负责把bitcode编译为x86指令集的机器码
- 链接器(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工具链,由两部分构成:
- 前端(emcc,Emscripten Compiler Frontend): 将C/C++编译成LLVM的IR, why clang ? 因为emcc将会提供一些独特的功能比如说宏定义等
- 后端(Fastcomp) 将LLVM中间代码编译到目标语言(js)
WASM如何编译?
C/C++编译到WASM的方式:
- 远古方法:利用s2wasm(已弃用)将assembly转化成webAssembly。
C/C++ - {Clang} -> LLVM IR - {llc} -> *.s - {s2wasm} -> wasm
- 传统方法: 先编译到ASM.js再通过asm2wasm转化成webAssembly。
C/C++ - {emcc/Fastcomp} -> ASM.js - {asm2wasm} -> wasm
- 现代方法(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?
- emcc(dlmalloc): emscripten/library_syscall.js at incoming · emscripten-core/emscripten · GitHub
- wasmception(musl-libc): GitHub - yurydelendik/wasmception: Minimal C/C++ language toolset for building wasm files demo: WebAssembly Studio
- wasi(dlmalloc): GitHub - CraneStation/wasi-libc: WASI libc implementation for WebAssembly
其他语言的编译工具:
Binaryen
webassembly官方的套工具链,提供了一套binaryen IR与后端, 目前比较著名的前端有:
- AssemblyScript: Typescript
- Asterius: Haskell
- 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](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2020/1/15/16fa8eb859542d28~tplv-t2oaga2asx-watermark.awebp