
一 背景簡介
二 設計簡介
注:由於當前手機絕大多數都已支援 arm64,hyengine 僅支援 arm64 的 jit 實現。

compiler 部分分為前端、中端、後端,其中前端部分複用現有指令碼引擎的實現,比如 js 使用 quickjs,wasm 使用 emscripten,中端計劃實現一套自己的位元組碼、最佳化器及位元組碼轉換器,後端實現了 quickjs 和 wasm 的 jit 及彙編器和最佳化器。
vm 分為直譯器、runtime、api、除錯、基礎庫,由於人力有限,目前VM暫無完整的自有實現,複用quickjs/wasm3 的程式碼,透過實現一套自己的內分配器及gc,和最佳化現有runtime實現來提升效能。

c/c++ 程式碼經過 emscripten 編譯變為 wasm 檔案,wasm 經過 hyengine(wasm3) 載入並編譯為 arm64 指令,arm64 指令經過 optimizer 最佳化產出最佳化後的 arm64 指令,業務方透過呼叫入口 api 來執行對應程式碼。
注:本方案業界的方舟編譯器和 graalvm 可能有一定相似度。
三 實現介紹
1 編譯(compiler)部分
彙編器
彙編器的核心程式碼基於 golang 的 arch 專案已有的指令資料根據指令碼生成,並輔佐人工修正及對應的工具程式碼。
單個彙編程式碼示例如下:
// Name: ADC
// Arch: 32-bit variant
// Syntax: ADC <Wd>, <Wn>, <Wm>
// Alias:
// Bits: 0|0|0|1|1|0|1|0|0|0|0|Rm:5|0|0|0|0|0|0|Rn:5|Rd:5
staticinlinevoidADC_W_W_W(uint32_t *buffer, int8_t rd, int8_t rn, int8_t rm){
uint32_t code = 0b00011010000000000000000000000000;
code |= IMM5(rm) << 16;
code |= IMM5(rn) << 5;
code |= IMM5(rd);
*buffer = code;
}
其中IMM5實現如下:
// 0a011f1a| adc w10, w8, wzr
ADC_W_W_W(&buffer, R10, R8, RZR);
assert(buffer == bswap32(0x0a011f1a));
第一行註釋中前半部分為機器碼的大端表示,後半部分為機器碼對應的彙編程式碼。第二行為彙編器的彙編方法呼叫。第三行為彙編結果檢查,確認結果和註釋中的機器碼一致,由於註釋中的機器碼為大端表示,需要做 byte swap 才和彙編結果匹配。
反彙編器
#define IS_MOV_X_X(ins) \
(IMM11(ins 21) == IMM11(HY_INS_TEMPLATE_MOV_X_X >> 21) && \
IMM11(ins 5) == IMM11(HY_INS_TEMPLATE_MOV_X_X >> 5))
// e7031eaa| mov x7, x30
assert(IS_MOV_X_X(bswap32(0xe7031eaa)));
wasm編譯
其中核心的翻譯的整體實現是一個大的迴圈 + switch,每遍歷一個 opcode 即生成一段對應的機器碼,程式碼示例如下:
M3Result h3_JITFunction(IH3JITState state, IH3JITModule hmodule,
IH3JITFunction hfunction) {
uint32_t *alloc = state->code + state->codeOffset;
......
// prologue
// stp x28, x27, [sp, #-0x60]!
// stp x26, x25, [sp, #0x10]!
// stp x24, x23, [sp, #0x20]
// stp x22, x21, [sp, #0x30]
// stp x20, x19, [sp, #0x40]
// stp x29, x30, [sp, #0x50]
// add x20, sp, #0x50
STP_X_X_X_I_PR(alloc + codeOffset++, R28, R27, RSP, -0x60);
STP_X_X_X_I(alloc + codeOffset++, R26, R25, RSP, 0x10);
STP_X_X_X_I(alloc + codeOffset++, R24, R23, RSP, 0x20);
STP_X_X_X_I(alloc + codeOffset++, R22, R21, RSP, 0x30);
STP_X_X_X_I(alloc + codeOffset++, R20, R19, RSP, 0x40);
STP_X_X_X_I(alloc + codeOffset++, R29, R30, RSP, 0x50);
ADD_X_X_I(alloc + codeOffset++, R29, RSP, 0x50);
......
for (bytes_t i = wasm; i < wasmEnd; i += opcodeSize) {
uint32_t index = (uint32_t)(i - wasm) / sizeof(u8);
uint8_t opcode = *i;
......
switch (opcode) {
case OP_UNREACHABLE: {
BRK_I(alloc + codeOffset++, 0);
break;
}
case OP_NOP: {
NOP(alloc + codeOffset++);
break;
}
......
case OP_REF_NULL:
case OP_REF_IS_NULL:
case OP_REF_FUNC:
default:
break;
}
if (spOffset > maxSpOffset) {
maxSpOffset = spOffset;
}
}
......
// return 0(m3Err_none)
MOV_X_I(alloc + codeOffset++, R0, 0);
// epilogue
// ldp x29, x30, [sp, #0x50]
// ldp x20, x19, [sp, #0x40]
// ldp x22, x21, [sp, #0x30]
// ldp x24, x23, [sp, #0x20]
// ldp x26, x25, [sp, #0x10]
// ldp x28, x27, [sp], #0x60
// ret
LDP_X_X_X_I(alloc + codeOffset++, R29, R30, RSP, 0x50);
LDP_X_X_X_I(alloc + codeOffset++, R20, R19, RSP, 0x40);
LDP_X_X_X_I(alloc + codeOffset++, R22, R21, RSP, 0x30);
LDP_X_X_X_I(alloc + codeOffset++, R24, R23, RSP, 0x20);
LDP_X_X_X_I(alloc + codeOffset++, R26, R25, RSP, 0x10);
LDP_X_X_X_I_PO(alloc + codeOffset++, R28, R27, RSP, 0x60);
RET(alloc + codeOffset++);
......
return m3Err_none;
}
位元組碼生成機器碼以 wasm 的 opcode i32.add 為例:
case OP_I32_ADD: {
LDR_X_X_I(alloc + codeOffset++, R8, R19, (spOffset - 2) * sizeof(void *));
LDR_X_X_I(alloc + codeOffset++, R9, R19, (spOffset - 1) * sizeof(void *));
ADD_W_W_W(alloc + codeOffset++, R9, R8, R9);
STR_X_X_I(alloc + codeOffset++, R9, R19, (spOffset - 2) * sizeof(void *));
spOffset--;
break;
}
這段程式碼會生成 4 條機器碼,分別用於載入位於棧上spOffset – 2和spOffset – 1位置的兩條資料,然後相加,再把結果存放到棧上spOffset – 2位置。由於 i32.add 指令會消耗 2 條棧上資料,並生成 1 條棧上資料,最終棧的偏移就要 -1。
上述程式碼生成的機器碼及其對應助記形式如下:
f9400a68: ldr x8, [x19,
f9400e69: ldr x9, [x19,
0b090109: add w9, w8, w9
f9000a69: str x9, [x19,
以如下 fibonacci 的 c 程式碼:
uint32_t fib_native(uint32_t n) {
if (n < 2) return n;
return fib_native(n - 1) + fib_native(n - 2);
}
parse | load module: 61 bytes
parse | found magic + version
parse | ** Type [1]
parse | type 0: (i32) -> i32
parse | ** Function [1]
parse | ** Export [1]
parse | index: 0; kind: 0; export: 'fib';
parse | ** Code [1]
parse | code size: 29
compile |compiling:'fib'; wasm-size:29; numArgs:1; return: i32
compile | estimated constant slots: 3
compile | start stack index:1
compile | 0 |0x20 .. local.get
compile | 1 |0x41 .. i32.const
compile | | .......... (const i32 = 2)
compile | 2 |0x49 .. i32.lt_u
compile | 3 |0x04 .. if
compile | 4 |0x20 .... local.get
compile | 5 |0x0f .... return
compile | 6 |0x0b .. end
compile | 7 |0x20 .. local.get
compile | 8 |0x41 .. i32.const
compile | | .......... (const i32 = 2)
compile | 9 |0x6b .. i32.sub
compile | 10 |0x10 .. call
compile | | .......... (func= 'fib'; args= 1)
compile | 11 |0x20 .. local.get
compile | 12 |0x41 .. i32.const
compile | | .......... (const i32 = 1)
compile | 13 |0x6b .. i32.sub
compile | 14 |0x10 .. call
compile | | .......... (func= 'fib'; args= 1)
compile | 15 |0x6a .. i32.add
compile | 16 |0x0f .. return
compile | 17 |0x0bend
compile | unique constant slots: 2; unused slots: 1
compile | max stack slots:7
0x107384000: stp x28, x27, [sp, #-0x60]!
0x107384004: stp x26, x25, [sp, #0x10]
0x107384008: stp x24, x23, [sp, #0x20]
0x10738400c: stp x22, x21, [sp, #0x30]
0x107384010: stp x20, x19, [sp, #0x40]
0x107384014: stp x29, x30, [sp, #0x50]
0x107384018: add x29, sp, #0x50 ; =0x50
0x10738401c: mov x19, x0
0x107384020: ldr x9, [x19]
0x107384024: str x9, [x19, #0x8]
0x107384028: mov w9, #0x2
0x10738402c: str x9, [x19, #0x10]
0x107384030: mov x9, #0x1
0x107384034: ldr x10, [x19, #0x8]
0x107384038: ldr x11, [x19, #0x10]
0x10738403c: cmp w10, w11
0x107384040: csel x9, x9, xzr, lo
0x107384044: str x9, [x19, #0x8]
0x107384048: ldr x9, [x19, #0x8]
0x10738404c: cmp x9, #0x0 ; =0x0
0x107384050: b.eq 0x107384068
0x107384054: ldr x9, [x19]
0x107384058: str x9, [x19, #0x8]
0x10738405c: ldr x9, [x19, #0x8]
0x107384060: str x9, [x19]
0x107384064: b 0x1073840dc
0x107384068: ldr x9, [x19]
0x10738406c: str x9, [x19, #0x10]
0x107384070: mov w9, #0x2
0x107384074: str x9, [x19, #0x18]
0x107384078: ldr x8, [x19, #0x10]
0x10738407c: ldr x9, [x19, #0x18]
0x107384080: sub w9, w8, w9
0x107384084: str x9, [x19, #0x10]
0x107384088: add x0, x19, #0x10 ; =0x10
0x10738408c: bl 0x10738408c
0x107384090: ldr x9, [x19]
0x107384094: str x9, [x19, #0x18]
0x107384098: mov w9, #0x1
0x10738409c: str x9, [x19, #0x20]
0x1073840a0: ldr x8, [x19, #0x18]
0x1073840a4: ldr x9, [x19, #0x20]
0x1073840a8: sub w9, w8, w9
0x1073840ac: str x9, [x19, #0x18]
0x1073840b0: add x0, x19, #0x18 ; =0x18
0x1073840b4: bl 0x1073840b4
0x1073840b8: ldr x8, [x19, #0x10]
0x1073840bc: ldr x9, [x19, #0x18]
0x1073840c0: add w9, w8, w9
0x1073840c4: str x9, [x19, #0x10]
0x1073840c8: ldr x9, [x19, #0x10]
0x1073840cc: str x9, [x19]
0x1073840d0: b 0x1073840dc
0x1073840d4: ldr x9, [x19, #0x10]
0x1073840d8: str x9, [x19]
0x1073840dc: mov x0, #0x0
0x1073840e0: ldp x29, x30, [sp, #0x50]
0x1073840e4: ldp x20, x19, [sp, #0x40]
0x1073840e8: ldp x22, x21, [sp, #0x30]
0x1073840ec: ldp x24, x23, [sp, #0x20]
0x1073840f0: ldp x26, x25, [sp, #0x10]
0x1073840f4: ldp x28, x27, [sp], #0x60
0x1073840f8: ret
最佳化器
0x10268cfb8 <+0>: stp x20, x19, [sp, #-0x20]!
0x10268cfbc <+4>: stp x29, x30, [sp, #0x10]
0x10268cfc0 <+8>: add x29, sp, #0x10
0x10268cfc4 <+12>: mov x19, x0
0x10268cfc8 <+16>: cmp w0, #0x2
0x10268cfcc <+20>: b.hs 0x10268cfd8
0x10268cfd0 <+24>: mov w20, #0x0
0x10268cfd4 <+28>: b 0x10268cff4
0x10268cfd8 <+32>: mov w20, #0x0
0x10268cfdc <+36>: sub w0, w19, #0x1
0x10268cfe0 <+40>: bl 0x10268cfb8
0x10268cfe4 <+44>: sub w19, w19, #0x2
0x10268cfe8 <+48>: add w20, w0, w20
0x10268cfec <+52>: cmp w19, #0x1
0x10268cff0 <+56>: b.hi 0x10268cfdc
0x10268cff4 <+60>: add w0, w19, w20
0x10268cff8 <+64>: ldp x29, x30, [sp, #0x10]
0x10268cffc <+68>: ldp x20, x19, [sp], #0x20
0x10268d000 <+72>: ret
為了縮小差距,是時候做一些優化了。
1)最佳化器的主要流程

需要把方法體拆分為塊的原因之一在於,最佳化器可能會刪除或者增加程式碼,這樣跳轉指令的跳轉目標地址就會發生改變,需要重新計算跳轉目標,拆成塊後跳轉目標比較容易計算。
在塊拆分及最佳化 pass 的實現中,會用到前面提到反彙編器和彙編器,這也是整個最佳化器的核心依賴。
我們以前文中的程式碼的一部分為例子,做最佳化流程的介紹,首先是塊拆分:
; --- code block 0 ---
0x107384048: ldr x9, [x19, #0x8]
0x10738404c: cmp x9, #0x0 ; =0x0
-- 0x107384050: b.eq 0x107384068 ; b.eq 6
|
| ; --- code block 1 ---
| 0x107384054: ldr x9, [x19]
|0x107384058: str x9, [x19, #0x8]
| 0x10738405c: ldr x9, [x19, #0x8]
|0x107384060: str x9, [x19]
| 0x107384064: b 0x1073840dc
|
| ; --- code block 2 ---
-> 0x107384068: ldr x9, [x19]
0x10738406c: str x9, [x19, #0x10]
已經拆過了,就不再拆了。
; --- code block 0 ---
0x104934020: cmp w9, #0x2 ; =0x2
-- 0x104934024: b.hs 0x104934038 ; b.hs 5
|
| ; --- code block 1 ---
| 0x104934028: mov x9, x20
| 0x10493402c: mov x21, x9
| 0x104934030: mov x20, x9
| 0x104934034: b 0x104934068
|
| ; --- code block 2 ---
-> 0x104934038: subw22, w20, #0x2; =0x2
跳轉的目標地址的偏移也少 1,從 6 變為 5。
0x104934020: cmp w9, #0x2 ; =0x2
0x104934024: b.hs 0x104934038 ; b.hs 5
0x104934028: mov x9, x20
0x10493402c: mov x21, x9
0x104934030: mov x20, x9
0x104934034: b 0x104934068
0x104934038: sub w22, w20, #0x2 ; =0x2
2)關鍵最佳化之暫存器分配
暫存器分配有一些較為成熟的方案,常用的包括:基於 live range 的線性掃描記憶體分配,基於 live internal 的線性掃描記憶體分配,基於圖染色的記憶體分配等。在常見 jit 實現,會採用基於 live internal 的線性掃描記憶體分配方案,來做到產物效能和暫存器分配程式碼的時間複雜度的平衡。
為了實現的簡單性,hyengine 使用了一種非主流的極簡方案,基於程式碼訪問次數的線性掃描記憶體分配,用人話說就是:給程式碼中出現次數最多的棧偏移分配暫存器。
0x107384020: ldrx9, [x19]
0x107384024: strx9, [x19, #0x8]
0x107384028: movw9, #0x2
0x10738402c: strx9, [x19, #0x10]
0x107384030: movx9, #0x1
0x107384034: ldrx10, [x19, #0x8]
0x107384038: ldrx11, [x19, #0x10]
0x107384020: ldr x9, [x19] ; 偏移0沒變
0x107384024: mov x20, x9 ; 偏移8變成x20
0x107384028: mov w9, #0x2
0x10738402c: mov x21, x9 ; 偏移16變成x21
0x107384030: mov x9, #0x1
0x107384034: mov x10, x20 ; 偏移8變成x20
0x107384038: mov x11, x21 ; 偏移16變成x21
0x102db4000: stp x28, x27, [sp, #-0x60]!
0x102db4004: stp x26, x25, [sp, #0x10]
0x102db4008: stp x24, x23, [sp, #0x20]
0x102db400c: stp x22, x21, [sp, #0x30]
0x102db4010: stp x20, x19, [sp, #0x40]
0x102db4014: stp x29, x30, [sp, #0x50]
0x102db4018: add x29, sp, #0x50 ; =0x50
0x102db401c: mov x19, x0
0x102db4020: ldr x9, [x19]
0x102db4024: mov x20, x9
0x102db4028: mov x21, #0x2
0x102db402c: mov x9, #0x1
0x102db4030: cmp w20, w21
0x102db4034: csel x9, x9, xzr, lo
0x102db4038: mov x20, x9
0x102db403c: cmp x9, #0x0 ; =0x0
0x102db4040: b.eq 0x102db4054
0x102db4044: ldr x9, [x19]
0x102db4048: mov x20, x9
0x102db404c: str x20, [x19]
0x102db4050: b 0x102db40ac
0x102db4054: ldr x9, [x19]
0x102db4058: mov x21, x9
0x102db405c: mov x22, #0x2
0x102db4060: sub w9, w21, w22
0x102db4064: mov x21, x9
0x102db4068: add x0, x19, #0x10 ; =0x10
0x102db406c: str x21, [x19, #0x10]
0x102db4070: bl 0x102db4070
0x102db4074: ldr x21, [x19, #0x10]
0x102db4078: ldr x9, [x19]
0x102db407c: mov x22, x9
0x102db4080: mov x23, #0x1
0x102db4084: sub w9, w22, w23
0x102db4088: mov x22, x9
0x102db408c: add x0, x19, #0x18 ; =0x18
0x102db4090: str x22, [x19, #0x18]
0x102db4094: bl 0x102db4094
0x102db4098: ldr x22, [x19, #0x18]
0x102db409c: add w9, w21, w22
0x102db40a0: mov x21, x9
0x102db40a4: str x21, [x19]
0x102db40a8: nop
0x102db40ac: mov x0, #0x0
0x102db40b0: ldp x29, x30, [sp, #0x50]
0x102db40b4: ldp x20, x19, [sp, #0x40]
0x102db40b8: ldp x22, x21, [sp, #0x30]
0x102db40bc: ldp x24, x23, [sp, #0x20]
0x102db40c0: ldp x26, x25, [sp, #0x10]
0x102db40c4: ldp x28, x27, [sp], #0x60
0x102db40c8: ret
3)關鍵最佳化之暫存器引數傳遞
0x102db4068: add x0, x19, #0x10 ; =0x10
0x102db406c: str x21, [x19, #0x10]
0x102db4070: bl 0x102db4070
0x102db4074: ldr x21, [x19, #0x10]
而 arm64 的呼叫約定中,引數傳遞是透過暫存器來做的,這樣每次方法呼叫可以減少兩次記憶體訪問。
這裡把 wasm 的棧作為放入
x0, 第一個引數x22直接放入x1,方法呼叫後的返回值x0直接放入x22,最佳化後代碼如下:0x1057e405c: add x0, x19, #0x10 ; =0x10
0x1057e4060: mov x1, x22
0x1057e4064: bl 0x1057e4064
0x1057e4068: mov x22, x0
注:這裡因為給棧偏移 0 也分配了暫存器,所以暫存器的編號比最佳化前的程式碼多 1 。
同時將方法頭部取引數的程式碼從:
0x102db4020: ldr x9, [x19]
0x102db4024: mov x20, x9
0x1057e4020: mov x20, x1
這裡又減少了一次記憶體訪問和一條指令。
最佳化後最終完整的程式碼如下:
0x1057e4000: stp x28, x27, [sp, #-0x60]!
0x1057e4004: stp x26, x25, [sp, #0x10]
0x1057e4008: stp x24, x23, [sp, #0x20]
0x1057e400c: stp x22, x21, [sp, #0x30]
0x1057e4010: stp x20, x19, [sp, #0x40]
0x1057e4014: stp x29, x30, [sp, #0x50]
0x1057e4018: add x29, sp, #0x50 ; =0x50
0x1057e401c: mov x19, x0
0x1057e4020: mov x20, x1
0x1057e4024: mov x21, x20
0x1057e4028: mov x22, #0x2
0x1057e402c: mov x9, #0x1
0x1057e4030: cmp w21, w22
0x1057e4034: csel x9, x9, xzr, lo
0x1057e4038: mov x21, x9
0x1057e403c: cmp x9, #0x0 ; =0x0
0x1057e4040: b.eq 0x1057e404c
0x1057e4044: mov x21, x20
0x1057e4048: b 0x1057e409c
0x1057e404c: mov x22, x20
0x1057e4050: mov x23, #0x2
0x1057e4054: sub w9, w22, w23
0x1057e4058: mov x22, x9
0x1057e405c: add x0, x19, #0x10 ; =0x10
0x1057e4060: mov x1, x22
0x1057e4064: bl 0x1057e4064
0x1057e4068: mov x22, x0
0x1057e406c: mov x23, x20
0x1057e4070: mov x24, #0x1
0x1057e4074: sub w9, w23, w24
0x1057e4078: mov x23, x9
0x1057e407c: add x0, x19, #0x18 ; =0x18
0x1057e4080: mov x1, x23
0x1057e4084: bl 0x1057e4084
0x1057e4088: mov x23, x0
0x1057e408c: add w9, w22, w23
0x1057e4090: mov x22, x9
0x1057e4094: mov x20, x22
0x1057e4098: nop
0x1057e409c: mov x0, x20
0x1057e40a0: ldp x29, x30, [sp, #0x50]
0x1057e40a4: ldp x20, x19, [sp, #0x40]
0x1057e40a8: ldp x22, x21, [sp, #0x30]
0x1057e40ac: ldp x24, x23, [sp, #0x20]
0x1057e40b0: ldp x26, x25, [sp, #0x10]
0x1057e40b4: ldp x28, x27, [sp], #0x60
0x1057e40b8: ret
注:這個最佳化僅對方法體比較短且呼叫頻繁的方法有顯著跳過,方法體比較長的程式碼效果不明顯。
4)關鍵最佳化之特徵匹配
特徵匹配就是在程式碼中遍歷預設的程式碼特徵,對符合特徵的程式碼做相應的最佳化。
比如上面程式碼中的:
0x1057e404c: mov x22, x20
0x1057e4050: mov x23, #0x2
0x1057e4054: sub w9, w22, w23
0x1057e4058: mov x22, x9
0x104934038: subw22, w20, #0x2; =0x2
5)最佳化結果
0x104934000: stp x24, x23, [sp, #-0x40]!
0x104934004: stp x22, x21, [sp, #0x10]
0x104934008: stp x20, x19, [sp, #0x20]
0x10493400c: stp x29, x30, [sp, #0x30]
0x104934010: add x29, sp, #0x30 ; =0x30
0x104934014: mov x19, x0
0x104934018: mov x20, x1
0x10493401c: mov x9, x20
0x104934020: cmp w9, #0x2 ; =0x2
0x104934024: b.hs 0x104934038
0x104934028: mov x9, x20
0x10493402c: mov x21, x9
0x104934030: mov x20, x9
0x104934034: b 0x104934068
0x104934038: sub w22, w20, #0x2 ; =0x2
0x10493403c: add x0, x19, #0x10 ; =0x10
0x104934040: mov x1, x22
0x104934044: bl 0x104934000
0x104934048: mov x22, x0
0x10493404c: sub w23, w20, #0x1 ; =0x1
0x104934050: add x0, x19, #0x18 ; =0x18
0x104934054: mov x1, x23
0x104934058: bl 0x104934000
0x10493405c: add w9, w22, w0
0x104934060: mov x22, x9
0x104934064: mov x20, x9
0x104934068: mov x0, x20
0x10493406c: ldp x29, x30, [sp, #0x30]
0x104934070: ldp x20, x19, [sp, #0x20]
0x104934074: ldp x22, x21, [sp, #0x10]
0x104934078: ldp x24, x23, [sp], #0x40
0x10493407c: ret
quickjs 編譯
quickjs 的編譯流程和 wasm 類似,只是對 opcode 的實現上會稍微複雜一些,以OP_object為例:
// *sp++ = JS_NewObject(ctx);
// if (unlikely(JS_IsException(sp[-1])))
// goto exception;
case OP_object: {
MOV_FUNCTION_ADDRESS_TO_REG(R8, JS_NewObject);
MOV_X_X(NEXT_INSTRUCTION, R0, CTX_REG);
BLR_X(NEXT_INSTRUCTION, R8);
STR_X_X_I(NEXT_INSTRUCTION, R0, R26, SP_OFFSET(0));
CHECK_EXCEPTION(R0, R9);
break;
}
#define MOV_FUNCTION_ADDRESS_TO_REG(reg, func) \
{ \
uintptr_t func##Address = (uintptr_t)func; \
MOVZ_X_I_S_I(NEXT_INSTRUCTION, reg, IMM16(func##Address), LSL, 0); \
if (IMM16(func##Address >> 16) != 0) { \
MOVK_X_I_S_I(NEXT_INSTRUCTION, reg, IMM16(func##Address >> 16), \
LSL, 16); \
} else{ \
NOP(NEXT_INSTRUCTION); \
} \
if (IMM16(func##Address >> 32) != 0) { \
MOVK_X_I_S_I(NEXT_INSTRUCTION, reg, IMM16(func##Address >> 32), \
LSL, 32); \
} else{ \
NOP(NEXT_INSTRUCTION); \
} \
if (IMM16(func##Address >> 48) != 0) { \
MOVK_X_I_S_I(NEXT_INSTRUCTION, reg, IMM16(func##Address >> 48), \
LSL, 48); \
} else{ \
NOP(NEXT_INSTRUCTION); \
} \
}
#define EXCEPTION(tmp) \
tmp, CTX_REG, HYJS_BUILTIN_OFFSET(0)); \
MOV_X_I(NEXT_INSTRUCTION, R0, SP_OFFSET(0)); \
BLR_X(NEXT_INSTRUCTION, tmp);
#define CHECK_EXCEPTION(reg, tmp) \
tmp, ((uint64_t)JS_TAG_EXCEPTION<<56)); \
CMP_X_X_S_I(NEXT_INSTRUCTION, reg, tmp, LSL, 0); \
B_C_L(NEXT_INSTRUCTION, NE, 4 * sizeof(uint32_t)); \
EXCEPTION(tmp)
同樣是 fibonacci 的實現,wasm 的 jit 產物程式碼只有 32 條,而 quickjs 的有 467 條!!!又想起了被彙編所支配的恐懼。
注:這麼指令源於對 builtin 的呼叫、引用計數、型別判斷。後面 vm 最佳化將引用計數幹掉後代碼量減少到 420 條。
2 引擎(vm)部分
在之前對某業務js程式碼的效能分析後發現,超過 50% 的效能開銷在記憶體分配及 gc 上,為此引擎部分將主要介紹對 quickjs 的記憶體分配和 gc 最佳化,部分 runtime 的 builtin 的快路徑、inline cache 目前最佳化佔比不高,僅做少量介紹。
記憶體分配器 hymalloc
1)實現簡介
每個區可包含多個池(pool),每個池裡面可包含多個目標大小的條目(item)。large region 比較特殊,每個 pool 裡只有 1 個條目。在向系統申請記憶體時,按 pool 來做申請,之後再將 pool 拆分成對應的 item。
每個 small region 初始化有一個池,池的大小可配置,預設為 1024 個 item;large region 預設是空的。
區/塊/池的示意圖如下:

// hymalloc item
structHYMItem {
union {
HYMRegion* region; // set to region when allocated
HYMItem* next; // set to next free item when freed
};
size_t flags;
uint8_t ptr[0];
};
// hymalloc pool
structHYMPool {
HYMRegion *region;
HYMPool *next;
size_t item_size;
};
被設定為 item 所屬的 region 地址。
region 的空閒 item 連結串列示意圖如下:


static void* _HYMallocFixedSize(HYMRegion *region, size_t size) {
// allocate new pool, if no free item exists
if (region->free_item_list == NULL) {
// notice: large region's item size is 0, use 'size' instead
size_t item_size = region->item_size ? region->item_size : size;
int ret = _HYMAllocPool(region, region->pool_initial_item_count, item_size);
if (!ret) {
returnNULL;
}
}
// get free list item head, and set region to item's region
HYMItem *item = region->free_item_list;
region->free_item_list = item->next;
item->region = region;
item->flags = 0;
return &item->ptr;
}
voidHYMFree(void *ptr){
HYMItem *item = (HYMItem *)((uint8_t *)ptr - HYM_ITEM_SIZE_OVERHEAD);
// set item as head of region's free item list
HYMRegion *region = item->region;
HYMItem *first_item_in_region = region->free_item_list;
region->free_item_list = item;
item->next = first_item_in_region;
}
2)記憶體 compact + update
但從客戶端的使用場景來看,執行程式碼的記憶體用量本身不高,compact + update 完整組合的實現複雜度較高,價效比不足。後續根據實際業務的使用情況,再評估實現完整 compact + update 的必要性。
3)hymalloc 的侷限性
而且雖然 hymalloc 提供了 compact 方法來釋放空閒的記憶體,但由於按照 pool 來批次申請記憶體,只要 pool 中有一個 item 被使用,那麼這個 pool 就不會被釋放,導致記憶體不能被完全高效的釋放。
另外,考慮到記憶體被複用的機率,large region 的記憶體會預設按 256bytes 對齊來申請,同樣可能存在浪費。
上述問題可以透過設定更小的 pool 的預設 item 數量,及更小的對齊尺寸,犧牲少量效能,來減少記憶體浪費。
後續可以引入更合理的資料結構,以及更完善的 compact + update 機制,來減少記憶體浪費。
垃圾回收器 hygc
為了實現 hyengine 對 quickjs 效能最佳化,減少 gc 在整體耗時種的佔比,減少 gc 可能導致的長時間執行停止。參考 v8 等其他先進引擎的 gc 設計思路,實現一套適用於移動端業務的,輕量級、高效能、實現簡單的 gc。
注:本實現僅僅針對於 quickjs,後續可能會衍生出通用的 gc 實現。
注:為了保障業務體驗不出現卡頓,需要將 gc 的暫停時間控制在 30ms 內。
1)常用垃圾回收實現
-
引用計數
-
給每個物件加一個引用數量,多一個引用數量 +1,少一個引用數量 -1,如果引用數量為 0 則釋放。 -
弊端:無法解決迴圈引用問題。
-
mark sweep
-
遍歷物件,標記物件是否有引用,如果沒有請用則清理掉。
-
複製 gc
-
遍歷物件,標記物件是否有引用,把有引用的物件複製一份新的,丟棄所有老的記憶體。
-
三色標記 gc
-
遍歷物件,標記物件是否有引用,狀態比單純的有引用(黑色)和無引用(白色)多一箇中間狀態標記中/不確定(灰色),可支援多執行緒。
2)hygc 的業務策略
-
無 gc
-
執行期不觸發 gc 操作。 -
待程式碼完全執行完畢銷燬 runtime 時做一次 full gc 整體釋放記憶體。
-
閒時 gc
-
執行期不觸發 gc 操作,執行結束後在非同步執行緒做 gc。 -
程式碼完全執行完畢銷燬 runtime 時做一次 full gc 整體釋放記憶體。
-
預設 gc
-
執行期會觸發 gc。 -
程式碼完全執行完畢銷燬 runtime 時做一次 full gc 整體釋放記憶體。
3)hygc 的實現方案
hygc 的三色標記流程(單執行緒版本):

首先,收集根物件的主要操作是掃描 js 執行緒的棧,並將執行緒棧上的 js 物件和 js 呼叫棧關聯的物件收集起來,作為三色標記的根物件。然後,從根物件作為標記入口,依次遞迴標記子物件。遍歷 gc_obj_list(quickjs 的所有需要 gc 的物件都在這個雙向連結串列上),將沒有被標記到的物件放入 tmp_obj_list。最後,釋放 tmp_obj_list 中的物件。

注:令人悲傷的是,由於 mark 和垃圾回收仍然只在單獨一個執行緒完成,這裡只用到了兩種顏色做標記,灰色實際上沒用到。後續最佳化讓 hygc 實現和 quickjs 原本的 gc 能夠共存,讓 gc 的遷移風險更低。
4)hygc 的侷限性
此問題後續也將根據實際情況,判斷是否進行方案最佳化來解決。
其他最佳化舉例
1)global 物件的 inline cache
global inline cache 的資料結構如下:
typedefstruct {
JSAtom prop; // property atom
int offset; // cached property offset
void *obj; // global_obj or global_var_obj
} HYJSGlobalIC;
中,具體存在哪個裡面需要一併放入 cache 中。
具體程式碼實現如下:
case OP_get_var: { // 73
JSAtom atom = get_u32(buf + i + 1);
uint32_t cache_index = hyjs_GetGlobalICOffset(ctx, atom);
JSObject obj;
JSShape shape;
LDR_X_X_I(NEXT_INSTRUCTION, R8, CTX_REG, (int32_t)((uintptr_t)&ctx->global_ic - (uintptr_t)ctx));
ADD_X_X_I(NEXT_INSTRUCTION, R8, R8, cache_index * sizeof(HYJSGlobalIC));
LDP_X_X_X_I(NEXT_INSTRUCTION, R0, R9, R8, 0);
CBZ_X_L(NEXT_INSTRUCTION, R9, 12 * sizeof(uint32_t)); // check cache exsits
LSR_X_X_I(NEXT_INSTRUCTION, R1, R0, 32); // get offset
LDR_X_X_I(NEXT_INSTRUCTION, R2, R9, (int32_t)((uintptr_t)&obj.shape - (uintptr_t)&obj)); // get shape
ADD_X_X_I(NEXT_INSTRUCTION, R2, R2, (int32_t)((uintptr_t)&shape.prop - (uintptr_t)&shape)); // get prop
LDR_X_X_W_E_I(NEXT_INSTRUCTION, R3, R2, R1, UXTW, 3); // get prop
LSR_X_X_I(NEXT_INSTRUCTION, R3, R3, 32);
CMP_W_W_S_I(NEXT_INSTRUCTION, R0, R3, LSL, 0);
B_C_L(NEXT_INSTRUCTION, NE, 5 * sizeof(uint32_t));
LDR_X_X_I(NEXT_INSTRUCTION, R2, R9, (int32_t)((uintptr_t)&obj.prop - (uintptr_t)&obj)); // get prop
LSL_W_W_I(NEXT_INSTRUCTION, R1, R1, 4); // R1 * sizeof(JSProperty)
LDR_X_X_W_E_I(NEXT_INSTRUCTION, R0, R2, R1, UXTW, 0); // get value
B_L(NEXT_INSTRUCTION, 17 * sizeof(uint32_t));
MOV_FUNCTION_ADDRESS_TO_REG(R8, HYJS_GetGlobalVar);
MOV_X_X(NEXT_INSTRUCTION, R0, CTX_REG);
MOV_IMM32_TO_REG(R1, atom);
MOV_X_I(NEXT_INSTRUCTION, R2, opcode - OP_get_var_undef);
MOV_X_I(NEXT_INSTRUCTION, R3, cache_index);
BLR_X(NEXT_INSTRUCTION, R8);
CHECK_EXCEPTION(R0, R9);
STR_X_X_I(NEXT_INSTRUCTION, R0, R26, SP_OFFSET(0));
i += 4;
break;
}
2)builtin 的快路徑最佳化
以 Array.indexOf 的實現為例:
staticJSValue hyjs_array_indexOf(JSContext *ctx, JSValueConst func_obj,
JSValueConstobj,
intargc, JSValueConst *argv, int flags)
{
......
res = -1;
if(len > 0) {
......
fast path
if(JS_VALUE_GET_TAG(element) == JS_TAG_INT) {
for(; n < count; n++) {
if(JS_VALUE_GET_PTR(arrp[n]) == JS_VALUE_GET_PTR(element)) {
res = n;
gotodone;
}
}
gotoproperty_path;
}
slow path
for(; n < count; n++) {
if(js_strict_eq2(ctx, JS_DupValue(ctx, argv[0]),
arrp[n]), JS_EQ_STRICT)) {
res = n;
gotodone;e
}
}
......
}
done:
returnJS_NewInt64(ctx, res);
exception:
returnJS_EXCEPTION;
}
四 最佳化結果
1 wasm 效能
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
注:coremark hyengine jit 耗時是 llvm 編譯版本的約 3 倍,原因在於對計算指令最佳化不足,後續可在最佳化器中對更多計算指令進行最佳化。
注:上述測試編譯最佳化選項為 O3。
2 js效能
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
注:從業務 case 上可以看出,vm 最佳化所帶來的提升遠大於目前 jit 帶來的提升,原因在於 jit 目前引入的最佳化方式較少,仍有大量的最佳化空間。另外 case 1 在 v8 上,jit 比 jitless 帶來的提升也只有 30% 左右。在 jit 的實現中,單項的最佳化單來可能帶來的提升只有 1% 不到,需要堆幾十上百個不同的最佳化,來讓效能做到比如 30% 的提升,後續會更具效能需求及開發成本來做均衡選擇。
注:上述測試編譯最佳化選項為 Os。
五 後續計劃
後續計劃主要分為 2 個方向:效能最佳化、多語言支援,其中效能最佳化將會持續進行。
效能最佳化點包括:
-
編譯器最佳化,引入自有位元組碼支援。 -
最佳化器最佳化,引入更多最佳化pass。 -
自有 runtime,熱點方法彙編實現。
六 參考內容
-
wasm3: https://github.com/wasm3/wasm3 -
quickjs: https://bellard.org/quickjs/
-
v8: https://chromium.googlesource.com/v8/v8.git -
javascriptcore: https://opensource.apple.com/tarballs/JavaScriptCore/
-
golang/arch: https://github.com/golang/arch -
libmalloc: https://opensource.apple.com/tarballs/libmalloc/
-
Trash talk: the Orinoco garbage collector: https://v8.dev/blog/trash-talk -
JavaScript engine fundamentals: Shapes and Inline Caches:https://mathiasbynens.be/notes/shapes-ics
-
cs143: https://web.stanford.edu/class/cs143/ -
C in ASM(ARM64):https://www.zhihu.com/column/c_142064221
終端體驗平臺團隊立足於移動中臺定位,致力於無線端到端前沿技術探索,打造集團先進且行業領先的移動基礎設施及配套服務,為阿里巴巴超過 1100+ 年度活躍 App 提供研發支撐,是阿里集團移動技術生態的基石團隊之一,也是大淘寶核心技術團隊及雙 11 主戰部隊。
熱烈歡迎志同道合之士加入我們,聯絡郵箱:zhibing.lwh#alibaba-inc.com(傳送郵件時,請把#替換成@)