contributed by <tina0405
、workfunction
、LinRiver
>
Javascript : 文中提到,如果我們想要了解他們之間差在哪?就必須從 JS 引擎的運作開始了解(此圖並非精準的時間軸而是用來比較和 WebAssembly 之間的差異而已)
WebAssembly:
解碼 (Decode): 因為是 Binary Format 所以不用經過直譯器可直接解碼
編譯和優化 (Compiling + optimizing): 初級編譯器和優化編譯器花時間。
執行 (Execution): 執行所花時間
省下重優化 (Re-optimizing) 的原因:
在 WebAssembly 中,類型都是固定的,所以 JIT 不用根據變數類型作優化假設,所以沒有重優化階段。
省下垃圾回收 (Garbage collection) 的原因:
目前為止 WebAssembly 不支持垃圾回收 (Garbage collection) 記憶體操作變成使用者要自行負擔成本,這樣雖然對開發者不方便,但執行效率卻提高。
Asm.js: 是 WebAssembly 的前一階段,所以在記憶體處理這方面也是使用者自行操作,差別在於 WebAssembly 以完全跳脫 Javascript 架構
C/C++ 轉成 asm.js 的過程
WebAssembly 因為是二進位制的編碼,比起 Asm.js 人類可讀的文本來說,運轉速度更快體積更小
在 Mozilla 推出 Asm.js 架構前, 網頁程式需要使用 JIT(Just in Time) 逐行編譯JavaScript 程式碼, 因而影響了執行效能
雖然在推出 Asm.js 架構後提昇了執行效能, 卻也限制了 JavaScript 某些功能, 主要是去除了會影響執行效能的功能後來進行優化
為了改善上述困擾, 2015年6月由Chrome、Edge和Firefox三大瀏覽器及Safari的WebKit的工程師們聯手推動的新網頁格式標準WebAssembly發布, 它可作為一個網站程式碼的新中介層, 可以提供一個二進位檔案格式標準來執行網頁. 更準確來說, WebAssembly 直接定義了一個新的網頁執行層, 讓瀏覽器能直接執行二進位檔案格式的 WebAssembly 檔案, 來達到像執行 Bytecode 程式般的高效能
要補上參考資料來源喔!
tina0405
0061 736d 0100 0000 0104 0160 0000 0302
0100 0801 000a 1001 0e00 410a 4105 6e00
410b 4110 6b00 0b0a
(module
(func $main
i32.const 10
i32.const 5
unreachable
)
(start $main)
)
先複製
git clone git@github.com:WebAssembly/wabt.git
進入 wabt 資掉夾並建立 build 資料夾
cd wabt && mkdir build
進入 build 資料夾執行 CMake
cd build && cmake -DBUILD_TESTS=OFF ..
執行 wat2wasm 檔,使用方法如下: (-v 是為了方便查看位元檔內容)
./wat2wasm 輸入文字檔檔名 -o 輸出位元格式檔名 -v
wasm2wat
的反轉模式./WasmVM 位元格式檔
宣告方式 | 整數/小數 | 類型 |
---|---|---|
i32.const | 整數 | 32 位元 |
i64.const | 整數 | 64 位元 |
f32.const | 小數 | 單精度浮點數 |
f64.const | 小數 | 雙精度浮點數 |
使用16進位及科學記號都是被允許的
(module
(func $main
i32.const 2
i64.const 17
i64.const 0x11
f32.const -0.25
f32.const -inf
f32.const nan
unreachable
)
(start $main)
)
./WasmVM 數入位元格式檔案
的結果tina@tina-X550VB:~/Hw/WasmVM/build$ ./WasmVM ~/Hw/wabt/build/ttest
Values in the stack:
Type: f32, Value: nan
Type: f32, Value: -inf
Type: f32, Value: -0.25
Type: i64, Value: 17
Type: i64, Value: 17
Type: i32, Value: 2
我們會發現其常數的確是以堆疊的形式擺放,先進的後出
有了常數的類型,我們可以繼續往下運算:
型態 | 解釋 |
---|---|
i32.clz | 由左邊數來碰到 1 前會遇到幾個 0 |
i32.ctz | 由右邊數來碰到 1 前會遇到幾個 0 |
i32.popcnt | 計算整組位元中有幾個 1 |
i32.eqz | 這個整數是否為 0,是 -> 1,不是 -> 0 |
(module
(func $main
i32.const 2248752 ;; 00000000 00100010 01010000 00110000
i32.clz
i32.const 2248752
i32.ctz
unreachable
i32.const 2248752
i32.popcnt
unreachable
i32.const 2248752
i32.eqz
unreachable
i32.const 0
i32.eqz
unreachable
)
(start $main)
)
結果: 會發現 unreachable 會將多型態堆疊 (polymorphic stack) 的值先 pop 出去
tina@tina-X550VB:~/Hw/WasmVM/build$ ./WasmVM ~/Hw/wabt/build/ttest
Values in the stack:
Type: i32, Value: 4
Type: i32, Value: 10
Values in the stack:
Type: i32, Value: 6
Values in the stack:
Type: i32, Value: 0
Values in the stack:
Type: i32, Value: 1
四則運算 | 解釋 |
---|---|
i32.add | 兩數相加 |
i32.sub | 兩數相減 |
i32.mul | 兩數相乘 |
i32.div_s | 兩數當作有號整數相除,取商數,捨去小數部份 |
i32.div_u | 兩數當作無號整數相除,取商數,捨去小數部份 |
i32.rem_s | 兩數當作有號整數相除,取餘數 |
i32.rem_u | 兩數當作無號整數相除,取餘數 |
測試
;;註解
註解的格式)(module
(func $main
i32.const 10 ;; A
i32.const 5 ;; B
i32.div_u ;; A/B
unreachable
i32.const 11 ;; A
i32.const 16 ;; B
i32.sub ;; A-B
unreachable
)
(start $main)
)
Values in the stack:
Type: i32, Value: 2
Values in the stack:
Type: i32, Value: -5
另外在邏輯運算的部份:
先放入的統稱 A ,後放的統稱 B
邏輯運算 | 解釋 |
---|---|
i32.or | 位元和位元之間個別做 or 的運算 |
i32.and | 位元和位元之間個別做 and 的運算 |
i32.xor | 位元和位元之間個別做 xor 的運算 |
i32.shl | A 的位元向左移 B 位,並在右邊補 0 |
i32.shr_u | A 的位元向右移 B 位,並在左邊補 0 |
i32.shr_s | A 的位元向右移 B 位,如果 a 數的 sign 是 0 的話補 0,1 的話補 1 |
i32.rotl | A 的位元向左移 B 位,然後把超出去的位元補回右邊 |
i32.rotr | A 的位元向右移 B 位,然後把超出去的位元補回左邊 |
測試:
;;註解
註解的格式)(module
(func $main
i32.const 10 ;;0000 0000 0000 0000 0000 0000 0000 1010
i32.const 5 ;;0000 0000 0000 0000 0000 0000 0000 0101
i32.or
unreachable
i32.const 10 ;;0000 0000 0000 0000 0000 0000 0000 1010
i32.const 5 ;;0000 0000 0000 0000 0000 0000 0000 0101
i32.shl
unreachable
i32.const 10 ;;0000 0000 0000 0000 0000 0000 0000 1010
i32.const 5 ;;0000 0000 0000 0000 0000 0000 0000 0101
i32.rotr
unreachable
)
(start $main)
)
tina@tina-X550VB:~/Hw/WasmVM/build$ ./WasmVM ~/Hw/wabt/build/ttest
Values in the stack:
Type: i32, Value: 15
Values in the stack:
Type: i32, Value: 320
Values in the stack:
Type: i32, Value: 1342177280
block $aaa
,方便之後的 br 指令操作,而後面提到 br 1
和 br 0
的意思是:
br 0
: 跳到 0+1 層外 (if…else外的 i32.const3)br 1
: 跳到 1+1 層外(loop 外的 i32.const5)drop
: 從堆疊中拿出一個數值,然後捨棄不用if...else...end
的一定要判斷式像下圖, 上方只是 block
的示意if (result i32)
i32.const 2
else
i32.const 3
end
(module
(func $main
block $aaa
i32.const 8
i32.const 5
drop
unreachable
end
)
(start $main)
)
tina@tina-X550VB:~/Hw/WasmVM/build$ ./WasmVM ~/Hw/wabt/build/ttest
Values in the stack:
Type: i32, Value: 8
每一個區域變數,都會有一個編號從 0,1,2,3… ,宣告像:(local i32) 就是 0
每一個全域變數,都會有一個編號從 0,1,2,3…,宣告像: (global i32 i32.const 5)
一般宣告都是不可變動的,如果加上 mut
例如: (global (mut i32) i32.const 5) ,就代表之後 set_global
指令可以使用。
以下操作方式,在後面加上數字,表示要操作第幾個數
get_global
: 將全域變數的值放進堆疊裡set_global
: 從堆疊裡取出一個數值,放進全域變數get_local
: 將區域變數的值放進堆疊裡set_local
: 從堆疊裡取出一個數值,放進區域變數tee_local
: 從堆疊裡取出一個數值,放進區域變數,不過數值會再放回堆疊裡於是我們來跑跑看以下兩者差別
第一版:
(module
(func (local i32)
i32.const 7
i32.const 6
set_local 0
get_local 0
unreachable
)
)
第二版:
(module
(func (local i32)
i32.const 7
i32.const 6
tee_local 0
get_local 0
unreachable
)
)
兩者比較的結果:
第一版:
tina@tina-X550VB:~/Hw/WasmVM/build$ ./WasmVM ~/Hw/wabt/build/ttest
Values in the stack:
Type: i32, Value: 6
Type: i32, Value: 7
第二版:使用 tee_local
多一個6,因為放入區域變數後再放回 stack
tina@tina-X550VB:~/Hw/WasmVM/build$ ./WasmVM ~/Hw/wabt/build/ttest
Values in the stack:
Type: i32, Value: 6
Type: i32, Value: 6
Type: i32, Value: 7
Webassembly 的記憶體是以 byte 為單位的連續空間,初始值從0開始, 擴充時以64x1024 bytes 為單位進行, 此一單位稱為 page
Webassembly 在使用記憶體時須宣告, 如 Webassembly.memory(), 其本身也是 JavaScript 的物件, Webassembly 和 JavaScript 可以直接使用 memory 互相傳值. 經宣告後一模組至多只有一個記憶體, 如下說明:
(module
(memory 1 5)
)
在此模組中, 第一個數字代表記憶體之最小值, 同時表示一開始的大小. 第二個數字為最大值, 可自行決定是否設定. 若無, 則此塊記憶體成長無上限, 預增加其記憶體可使用 grow 方法.
Webassembly 宣告的記憶體與 JS 相互傳值, 由 JS 引擎建立一個 ArrayBuffer, 其記憶體大小透過使用者設定. 其記憶體做了兩件事, 一件是做 Webassembly 的記憶體, 另一是做 JavaScript 的物件.
因為如此, JavaScript 也可以獲取這個記憶體中的字元, 這樣的方式使得 Webassembly 和 JavaScript 可以共享記憶體來相互傳值.
兩者之間由陣列的索引來讀取記憶體, 而不是記憶體位址, 如下一簡單圖示說明:
首先 Webassembly 想要將一個字串如 Hello 寫入至記憶體, 需要將字串轉換成字元碼並且存入記憶體陣列中
將字串存在記憶體位置中的第一個位置, 也就是所對應到記憶體陣列的某一個索引傳給 JavaScript. 藉此, JavaScript 便可以在 ArrayBuffer 中依索引讀取字串
以上方式可以在 JS 文件中加入一些函數來協助, 例如 Emscription
因為 Webassembly 為一 JavaScript 物件, 具備了記憶體隔離, 防止瀏覽器中記憶體洩漏
以下將說明記憶體隔離與瀏覽器記憶體洩漏
記憶體洩漏
當自行管理記憶體有可能忘記進行清除, 導致系統記憶體不足. 若是一 Webassembly 模組的實體直接存入記憶體卻超出容量限制, 此時瀏覽器可能發生記憶體洩漏情形.但是, 因為 Webassembly.memory() 本身為 JavaScript 物件, 受到垃圾回收器追蹤. 若是移除 Webassembly 模組的實體時, 其所有記憶體也會被回收.
記憶體隔離
前面提到的 ArrayBuffer 會設定好記憶體陣列的範圍, 這意味著 Webassembly 模組的實體可直接管理的記憶體是受限, 只能管理受限內的記憶體而無法知道任何超出此範圍的記憶體內容. 具體來說, 每當 Webassembly 中有操作記憶體時, JS 引擎會進行記憶體陣列範圍檢查. 如果超出範圍, 引擎會拋出異常訊號來保護記憶體中的其他部份. 以下圖示為說明:
函式表為放有函式位置的表,可依據函式表上記錄的位置來間接呼叫函式. 此概念若在C語言中稱為函式指針.
在 web 頁面中, 所以的方法都為 JavaScript 物件並且將它們儲放於 Webassembly 記憶體之外的記憶體地址中. 下列圖示中紅色圓圈即為代表說明.
若想要一個變數來指向函式表中的函式, 便需要把其地址放入 Webassembly 的記憶體中
模組是 WebAssembly 程式發佈、載入和執行的基本單位. 當使用 JavaScript 啟動 Webassembly 模組, 會建立出一個模組實體(instance), 之後 JavaScript 程式碼可以呼叫該 Webassembly 模組上的實體函式.
Webassembly 模組的實體函式, Webassembly.instantiate, 受到呼叫時是先把從 wasm 抓到的二進位程式碼擷取到緩衝區內, 再進而提供給 Webassembly.instantiate, JS 引擎會把模組程式碼編譯為適合負責執行的機器碼格式.
除此之外, 產生出模組的實體還需要匯入(import)物件
每個模組需要特定的 imports 才能運作, import 有以下四種之一:
載入器 (Loader)
讀取檔案,將內容轉換成物件後載入儲存空間和模組
載入完之後就會被釋出,不會繼續留在程式裡
\0asm
)(module)
搭配 wabt 工具,就可以觀察到 Magic number
和 Version
的數值了,用法如下,參考Converting WebAssembly text format to wasm: tina@tina-X550VB:~/Hw/wabt/build$ ./wat2wasm Hw/wabt/build/webasm.wast -v
0000000: 0061 736d ; WASM_BINARY_MAGIC
0000004: 0100 0000 ; WASM_BINARY_VERSION
再來處理部份標頭 (Section header),和各個 section
英文名稱 | 代 碼 |
---|---|
Type | 1 |
Import | 2 |
Function | 3 |
Table | 4 |
Memory | 5 |
Global | 6 |
Export | 7 |
Start | 8 |
Element | 9 |
Code | 10 |
Data | 11 |
舉例來說:
(module
(func $main
i64.const 1
unreachable
)
(start $main)
)
0000000: 0061 736d ; WASM_BINARY_MAGIC
0000004: 0100 0000 ; WASM_BINARY_VERSION
; section "Type" (1)
0000008: 01 ; section code
0000009: 00 ; section size (guess)
000000a: 01 ; num types
; type 0
000000b: 60 ; func
000000c: 00 ; num params
000000d: 00 ; num results
0000009: 04 ; FIXUP section size
; section "Function" (3)
000000e: 03 ; section code
000000f: 00 ; section size (guess)
0000010: 01 ; num functions
0000011: 00 ; function 0 signature index
000000f: 02 ; FIXUP section size
; section "Start" (8)
0000012: 08 ; section code
0000013: 00 ; section size (guess)
0000014: 00 ; start func index
0000013: 01 ; FIXUP section size
; section "Code" (10)
0000015: 0a ; section code
0000016: 00 ; section size (guess)
0000017: 01 ; num functions
; function body 0
0000018: 00 ; func body size (guess)
0000019: 00 ; local decl count
000001a: 42 ; i64.const
000001b: 01 ; i64 literal
000001c: 00 ; unreachable
000001d: 0b ; end
0000018: 05 ; FIXUP func body size
0000016: 07 ; FIXUP section size
讀取檔案,將內容轉換成模組物件(moduleInst)
核心 (Core)
void Core::run(ModuleInst* moduleInst) {
// Invoke start function
Instruction::invoke(*(moduleInst->start), store, coreStack, moduleInst);
// Run
while (coreStack.curLabel != nullptr) {
Decoder::decode(store, coreStack);
}
}
解碼器 (Decoder)
switch (bincode) {
case OP_Ctrl_unreachable:
Instruction::ctrl_unreachable(store, coreStack);
break;
case OP_Ctrl_nop:
break;
...
} break;
系統呼叫 (System call)
tiny-syscall
這個分支,只有支援 3 個 system call:
static void sys_stdin(Store& store, Stack& coreStack);
static void sys_stdout(Store& store, Stack& coreStack);
static void sys_stderr(Store& store, Stack& coreStack);
Proc_5()
, 想利用 webassembly 的 function 形式, 做出虛擬機測試檔
Proc_5 () /* without parameters */
/*******/
/* executed once */
{
Ch_1_Glob = 'A';
Bool_Glob = false;
} /* Proc_5 */
mut
(module
(global (mut i32) i32.const 0)
(global (mut i32) i32.const 0)
(func $proc5
i32.const 65
i32.const 0
set_global 0
set_global 1
unreachable
)
(fun $proc0
call $proc5
)
(start $proc0)
)
proc_4
: 新增 1 個區域變數
Proc_4 () /* without parameters */
/*******/
/* executed once */
{
Boolean Bool_Loc;
Bool_Loc = Ch_1_Glob == 'A';
Bool_Glob = Bool_Loc | Bool_Glob;
Ch_2_Glob = 'B';
} /* Proc_4 */
equ
和 or
的運算,所以要在 stack
累積到兩個才可進行運算,而且也要確保 stack
裡面的值是正確的(module
(global (mut i32) i32.const 0)
(global (mut i32) i32.const 0)
(global (mut i32) i32.const 0)
(func $proc5
i32.const 65
i32.const 0
set_global 0
set_global 1
unreachable
)
(func $proc4
(local i32)
get_global 1
i32.const 65
i32.eq
if(result i32)
i32.const 1
else
i32.const 0
end
set_local 0
unreachable
get_local 0
get_global 0
i32.or
if(result i32)
i32.const 1
else
i32.const 0
end
set_global 0
unreachable
i32.const 66
set_global 2
)
(func $proc0
call $proc5
call $proc4
)
(start $proc0)
)