关于编译:hyengine-面向移动端的高性能通用编译解释引擎

9次阅读

共计 25019 个字符,预计需要花费 63 分钟才能阅读完成。

简介:手机淘宝客户端在历史上接过多种多样的脚本引擎,用于反对的语言包含:js/python/wasm/lua,其中 js 引擎接过的就有:javascriptcore/duktape/v8/quickjs 等多个。泛滥的引擎会面临独特面临包大小及性能相干的问题,咱们是否能够提供一套计划,在能反对业务需要的前提下,用一个引擎来反对尽可能多的语言,能较好的兼顾包大小较小和性能优异。为了解决这个问题,咱们开始了 hyengine 的摸索。

作者 | 知兵
起源 | 阿里技术公众号

一 背景简介

手机淘宝客户端在历史上接过多种多样的脚本引擎,用于反对的语言包含:js/python/wasm/lua,其中 js 引擎接过的就有:javascriptcore/duktape/v8/quickjs 等多个。泛滥的引擎会面临独特面临包大小及性能相干的问题,咱们是否能够提供一套计划,在能反对业务需要的前提下,用一个引擎来反对尽可能多的语言,能较好的兼顾包大小较小和性能优异。为了解决这个问题,咱们开始了 hyengine 的摸索。

二 设计简介

“ 有 hyengine 就够全家用了 ” – hyengine 是为对立挪动技术所需的各种脚本语言(wasm/js/python 等)执行引擎而生,以轻量级、高性能、多语言反对为设计和研发指标。目前已通过对 wasm3/quickjs 的 jit 编译及 runtime 优化,以极小包体积的代价实现了 wasm/js 执行速度 2~3 倍的晋升,将来将通过实现自有字节码和 runtime 减少对 python 及其他语言的反对。

注:因为以后手机绝大多数都已反对 arm64,hyengine 仅反对 arm64 的 jit 实现。

注:因为 ios 不反对 jit,目前 hyengine 只有 android 版本。

hyengine 整体分为两大块,编译(compiler)局部及引擎(vm)局部。

compiler 局部分为前端、中端、后端,其中前端局部复用现有脚本引擎的实现,比方 js 应用 quickjs,wasm 应用 emscripten,中端打算实现一套本人的字节码、优化器及字节码转换器,后端实现了 quickjs 和 wasm 的 jit 及汇编器和优化器。

vm 分为解释器、runtime、api、调试、根底库,因为人力无限,目前 VM 暂无残缺的自有实现,复用 quickjs/wasm3 的代码,通过实现一套本人的内分配器及 gc,和优化现有 runtime 实现来晋升性能。

业务代码(以 wasm 为例)通过下图所示的流程,被编译为可执行代码:

c/c++ 代码通过 emscripten 编译变为 wasm 文件,wasm 通过 hyengine(wasm3) 加载并编译为 arm64 指令,arm64 指令通过 optimizer 优化产出优化后的 arm64 指令,业务方通过调用入口 api 来执行对应代码。

注:hyengine 自身冀望积淀一套本人的底层(汇编级别)的根底能力库,除了用于 jit 相干用处外,还打算用于手机客户端的包大小、性能优化、调试辅助等场景。

注:本计划业界的方舟编译器和 graalvm 可能有肯定类似度。

三 实现介绍

1 编译(compiler)局部

为了让实现计划较为简单,hyengine 的编译采纳间接翻译的形式,间接翻译进去的代码性能个别较慢,须要通过优化器的优化来晋升性能。上面是相干模块的具体实现:

汇编器

为了生成 cpu 能执行的代码,咱们须要实现一个汇编器,将相干脚本的 opcode 翻译成机器码。

汇编器的外围代码基于 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
static inline void ADC_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;
}

代码的作用是汇编 ADC , , 指令,第一个参数是寄存机器码的 buffer,后三个参数别离为汇编指令的操作数 Wd/Wn/Wm。代码中第 7 行的 code 为机器码的固定局部,第 8~10 行为将操作数对应的寄存器编号放入机器码对应的地位(详见正文种的 Bits 局部),第 9 行为将机器码放入 buffer。其中 IMM5 示意取数值的低 5 位,因为寄存器是一个 5bits 长的数字。这样命名的益处是,能够直观的将汇编器的办法名和其产生的机器码的助记词模式相关联。

其中 IMM5 实现如下:

define IMM5(v) (v & 0b11111)

为了保障汇编办法的正确性,咱们基于 golang 的 arch 我的项目中的 gnucases.txt,采取机器生成 + 人工修改的形式,产出了如下格局的单测用例:

    // 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))

这条指令就能够在优化器中判断某条指令是不是 mov xd, xm,进而能够通过如下代码取出 xd 中 d 的具体数值:

#define RM(ins) IMM5(ins >> 16)

#define RN(ins) IMM5(ins >> 5)

#define RD(ins) IMM5(ins)

同样的,咱们为反汇编器也做了对应的单测:

// e7031eaa|    mov x7, x30
assert(IS_MOV_X_X(bswap32(0xe7031eaa)));

wasm 编译

编译时咱们会遍历 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;
}

上述代码会学生成办法的 prologue,而后 for 循环遍历 wasm 字节码,生产对应的 arm64 机器码,最初加上办法的 epilogue。

字节码生成机器码以 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;
}

代码中的 alloc 是以后正在编译的办法的机器码寄存首地址,codeOffset 是以后机器码绝对于首地址的偏移,R8/R9 代表咱们约定的两个长期寄存器,R19 寄存的栈底地址,spOffset 是运行到以后 opcode 时栈绝对于栈底的偏移。

这段代码会生成 4 条机器码,别离用于加载位于栈上 spOffset – 2 和 spOffset – 1 地位的两条数据,而后相加,再把后果寄存到栈上 spOffset – 2 地位。因为 i32.add 指令会耗费 2 条栈上数据,并生成 1 条栈上数据,最终栈的偏移就要 -1。

上述代码生成的机器码及其对应助记模式如下:

f9400a68: ldr    x8, [x19, #0x10]
f9400e69: ldr    x9, [x19, #0x18]
0b090109: add    w9, w8, w9
f9000a69: str    x9, [x19, #0x10]

x 示意 64 位寄存器,w 示意 64 位寄存器的低 32 位,因为 i32.add 指令是做 32 位加法,这里只须要加低 32 位即可。

以如下 fibonacci 的 c 代码:

uint32_t fib_native(uint32_t n) {if (n < 2) return n;
    return fib_native(n - 1) + fib_native(n - 2);
}

编译产生的 wasm 代码:

 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 | 0x0b   end
  compile  |  unique constant slots: 2; unused slots: 1
  compile  |  max stack slots: 7

通过 hyengine jit 编译的产出代码如下:

  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

这段代码运行 fib_native(40)耗时 1716ms,而 wasm3 解释执行 wasm 运行同样代码耗时 3637ms,耗时只有解释执行的约 47%,但这够快吗?

优化器

下面的代码看起来仿佛感觉没什么大故障,然而和 llvm 编译进去的 native 代码一比拟,差距就进去的了。fib_native 的 c 代码通过 llvm 编译的反汇编代码如下:

hyengine 产出的指令有 63 条,而 llvm 产出的指令只有 17 条,指令数量是 llvm 的约 3.7 倍!而理论运行性能差距更大,hyengine 产出的代码运行 fib_native(40)耗时 1716ms,llvm 产出的代码耗时 308ms,耗时是 llvm 的约 5.57 倍。

为了缩小差距,是时候做一些优化了。

1)优化器的次要流程

优化器的次要流程如下:

先将整个办法体的代码依照跳转指令 (如:b/cbz 等) 及其跳转指标地址做拆分,将办法体拆为多个代码块。而后对每个块跑一下优化的 pass,对块内代码进行优化。最初将优化后的指令块从新合并为一个办法体,优化实现。

须要把办法体拆分为块的起因之一在于,优化器可能会删除或者减少代码,这样跳转指令的跳转指标地址就会产生扭转,须要从新计算跳转指标,拆成块后跳转指标比拟容易计算。

在块拆分及优化 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]

这里会依据代码中的第四行的 b.eq 指令及其跳转的指标地址第 14 行作拆分,代码为拆为了 3 个块。本来第 11 行的 b 指令也要做一次拆分,但后面的 b.eq 曾经拆过了,就不再拆了。

接下对会对拆分成块后的代码跑一堆优化的 pass,跑完后的后果如下:

    ; --- 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: sub    w22, w20, #0x2            ; =0x2

在跑完一堆 pass 后代码齐全变了样(要害优化的实现请看下一节内容),但能够看出 code block 1 的代码从 5 条指令变成了 4 条,之前的 b.eq 被优化为了 b.hs 跳转的指标地址的偏移也少 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)要害优化之寄存器调配

3.7 倍代码量的速度慢 5.57 倍的一个次要起因在于,咱们生产的代码中数据齐全寄存在栈中,栈在内存上,各种 ldr/str 指令对内存的拜访,就算数据在 cpu 的 l1 cache 上,也比对寄存器的拜访慢 4 倍。为此,如果咱们将数据尽量放在寄存器,缩小对内存的拜访,就能够进一步晋升性能。

寄存器调配有一些较为成熟的计划,罕用的包含:基于 live range 的线性扫描内存调配,基于 live internal 的线性扫描内存调配,基于图染色的内存调配等。在常见 jit 实现,会采纳基于 live internal 的线性扫描内存调配计划,来做到产物性能和寄存器调配代码的工夫复杂度的均衡。

为了实现的简略性,hyengine 应用了一种非主流的极简计划,基于代码拜访次数的线性扫描内存调配,用人话说就是:给代码中呈现次数最多的栈偏移调配寄存器。

假如代码如下(节选自 hyengine jit 产出代码):

  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]

对假如代码的调配寄存器后代码如下:

 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

之前的 jit 产物代码优化后如下(注:做了大量指令交融):

 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

优化后的代码量从 63 条缩小到 51 条,且内存拜访数量显著缩小,耗时也缩小到 1361ms,耗时缩小到 llvm 的约 4.42 倍。

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

优化后的代码量从 51 条缩小到 47 条,耗时也缩小到 687ms,耗时缩小到 llvm 的约 2.23 倍。尽管代码量只缩小了 4 条,但耗时显著缩小了约 50%。

注:这个优化仅对办法体比拟短且调用频繁的办法有显著跳过,办法体比拟长的代码成果不显著。

4)要害优化之特色匹配

特色匹配就是在代码中遍历预设的代码特色,对合乎特色的代码做相应的优化。
比方下面代码中的:

0x1057e404c: mov    x22, x20
0x1057e4050: mov    x23, #0x2
0x1057e4054: sub    w9, w22, w23
0x1057e4058: mov    x22, x9

能够被优化为:

0x104934038: sub    w22, w20, #0x2            ; =0x2

4 条指令变 1 条。

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

优化后的代码量从 63 条缩小到 32 条,耗时从 1716ms 缩小到 493ms,耗时缩小到 llvm 的约 1.6 倍。

quickjs 编译

注:js 的次要耗时在 runtime,所以目前 jit 只占 js 整体性能优化的约 20%,后续将引入更多 jit 优化细节。

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;
}

这里首先通过 MOV_FUNCTION_ADDRESS_TO_REG 宏把要调用的 JS_NewObject 办法地址放入 R8 寄存器:

#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);                                             \
        }                                                                      \
    }

而后将 CTX_REG(外面存的 ctx 地址)放入 R0 作为第一个参数,并调用 JS_NewObject,而后后果存入 js 栈的 SP_OFFSET(0)地位。而后通过 CHECK_EXCEPTION 判断后果是否存在异样:

#define EXCEPTION(tmp)                                                     \
    LDR_X_X_I(NEXT_INSTRUCTION, 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)                                          \
    MOV_X_I(NEXT_INSTRUCTION, 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)

就这一个 opcode 生成的 arm64 机器码就多达 13 条!而且这还不算多的。

同样是 fibonacci 的实现,wasm 的 jit 产物代码只有 32 条,而 quickjs 的有 467 条!!!又想起了被汇编所摆布的恐怖。

注:这么指令源于对 builtin 的调用、援用计数、类型判断。前面 vm 优化将援用计数干掉后代码量减少到 420 条。

2 引擎(vm)局部

因为 wasm 自身是强类型的字节码,runtime 自身提供的能力较少,性能瓶颈也次要在代码的解释执行,所以 vm 局部的根本没有做优化。而 quickjs 的字节码作为弱类型的字节码,其次要性能须要依赖 runtime 来实现,同时因为语言自身接管了内存治理,由此带来的 gc 也开销也比拟显著。

在之前对某业务 js 代码的性能剖析后发现,超过 50% 的性能开销在内存调配及 gc 上,为此引擎局部将次要介绍对 quickjs 的内存调配和 gc 优化,局部 runtime 的 builtin 的快门路、inline cache 目前优化占比不高,仅做大量介绍。

内存分配器 hymalloc

为了实现 hyengine 对 quickjs 性能优化,同时兼顾 gc 优化所须要的对内存的管理权,须要设计一套更疾速(无锁,非线程平安)的内存分配器。同时须要思考面向其余引擎可能须要的定制,来做到 hymalloc 的尽量通用。

1)实现简介

hymalloc 将内存分为 19 个区(region),18 个 small region/1 个 large region。small region 次要用来寄存规定内存,每个区的大小分从为 116 至 1916 bytes;large region 用于寄存大于 9*16 bytes 的内存。

每个区可蕴含多个池(pool),每个池外面可蕴含多个指标大小的条目(item)。large region 比拟非凡,每个 pool 里只有 1 个条目。在向零碎申请内存时,按 pool 来做申请,之后再将 pool 拆分成对应的 item。

每个 small region 初始化有一个池,池的大小可配置,默认为 1024 个 item;large region 默认是空的。

区 / 块 / 池的示意图如下:

这里对最要害的两个数据结构做下简略介绍:

// hymalloc item
struct HYMItem {
    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
struct HYMPool {
    HYMRegion *region;
    HYMPool   *next;
    size_t    item_size;
};

其中 HYMItem 是后面提到的 item 的数据结构,这里的 item 的大小不固定,数据结构自身更像是 item header 形容,其中 flags 目前作为 gc 的特地标记存在,ptr 用于取 item 的理论可用局部内存的地址(通过 &item->ptr 获取)。union 中的 region/next 是一个用来省内存的设计,在 item 被调配进来之前,next 的值指向 region 的下一个闲暇 item;在 item 被调配进来之后,region 被设定为 item 所属的 region 地址。
region 的闲暇 item 链表示意图如下:

在内存调配时,取链表的首个 item 作为调配后果,链表如果为空,则向零碎申请一个新的 pool 并把 pool 的 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) {return NULL;}
    }
    
    // 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;
}

在内存开释时,将 item 插入所属 region 的闲暇链表的头部即可:

void HYMFree(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;

}

上述实现在简略的内存调配 / 开释测试 case 中,在 macbook m1 设施上比零碎提供的 malloc/free 快约 4 倍。

2)内存 compact + update

为了缩小内存占用,hymalloc 实现了局部内存 compact,能够清理齐全未应用的 small region 中的 pool 和 large region 的所有 pool。但目前没有实现 update 性能,无奈做到真正的将不同 pool 之间的 item 互相拷贝,来做到更多内存的节俭。

但从客户端的应用场景来看,运行代码的内存用量自身不高,compact + update 残缺组合的实现复杂度较高,性价比有余。后续依据理论业务的应用状况,再评估实现残缺 compact + update 的必要性。

3)hymalloc 的局限性

为了晋升调配和开释性能,hymalloc 的每个 item 都有 header,须要额定占用内存空间,这会导致肯定的内存节约。

而且尽管 hymalloc 提供了 compact 办法来开释闲暇的内存,但因为依照 pool 来批量申请内存,只有 pool 中有一个 item 被应用,那么这个 pool 就不会被开释,导致内存不能被齐全高效的开释。

另外,思考到内存被复用的概率,large region 的内存会默认按 256bytes 对齐来申请,同样可能存在节约。

上述问题能够通过设定更小的 pool 的默认 item 数量,及更小的对齐尺寸,就义大量性能,来缩小内存节约。

后续能够引入更正当的数据结构,以及更欠缺的 compact + update 机制,来缩小内存节约。

垃圾回收器 hygc

quickjs 的本来的 gc 基于援用计数 + mark sweep,设计和实现自身比拟简洁高效,但未实现分代、多线程、compact、闲时 gc、拷贝 gc,使得 gc 在整体执行耗时中的占比拟高,同时也存在内存碎片化带来的潜在性能升高。另外因为援用计数的存在,jit 生成的代码中会存在大量的援用计数操作的指令,使得代码体积较大。

为了实现 hyengine 对 quickjs 性能优化,缩小 gc 在整体耗时种的占比,缩小 gc 可能导致的长时间运行进行。参考 v8 等其余先进引擎的 gc 设计思路,实现一套实用于挪动端业务的,轻量级、高性能、实现简略的 gc。

注:本实现仅仅针对于 quickjs,后续可能会衍生出通用的 gc 实现。

注:为了保障业务体验不呈现卡顿,须要将 gc 的暂停工夫管制在 30ms 内。

1)罕用垃圾回收实现

罕用的垃圾回收次要有 3 大类:

援用计数

给每个对象加一个援用数量,多一个援用数量 +1,少一个援用数量 -1,如果援用数量为 0 则开释。

弊病:无奈解决循环援用问题。

mark sweep

遍历对象,标记对象是否有援用,如果没有请用则清理掉。
拷贝 gc

遍历对象,标记对象是否有援用,把有援用的对象拷贝一份新的,抛弃所有老的内存。

基于这三大类会有一些衍生,来实现多线程等反对,比方:

三色标记 gc

遍历对象,标记对象是否有援用,状态比单纯的有援用(彩色)和无援用(红色)多一个中间状态标记中 / 不确定(灰色),可反对多线程。

为了尽可能减少 gc 暂停工夫并缩小 js 执行耗时,hygc 采纳多线程三色 gc 计划。在业务 case 测试中,发现自身内存使用量并不大,故没有引入分代反对。

2)hygc 的业务策略

hygc 打算将策略能够裸露给用户,用于满足不同应用场景的性能需求,提供:无 gc、闲时 gc、多线程 gc 三种选项,应答不同场景对内存和性能的不同诉求。业务依据理论需要抉择 gc 策略,倡议对 gc 策略设置开关,防止所选的 gc 策略可能导致非预期的后果。

  • 无 gc

运行期不触发 gc 操作。

待代码齐全运行结束销毁 runtime 时做一次 full gc 整体开释内存。

  • 闲时 gc

运行期不触发 gc 操作,运行完结后在异步线程做 gc。

代码齐全运行结束销毁 runtime 时做一次 full gc 整体开释内存。

  • 默认 gc

运行期会触发 gc。

代码齐全运行结束销毁 runtime 时做一次 full gc 整体开释内存。

咱们的某个业务 case 就能够设定无 gc 或闲时 gc,因为代码运行期间没有内存能被回收,gc 是在浪费时间。

3)hygc 的实现计划

quickjs 本来采纳援用计数 + mark sweep 联合的 gc 计划,在 gc 优化时被移除,并替换为新的多线程三色标记 gc 计划。hygc 的实现复用了局部本来 quickjs 的代码,做到尽可能简略的实现所需性能。

hygc 的三色标记流程(单线程版本):

首先,收集根对象的次要操作是扫描 js 线程的栈,并将线程栈上的 js 对象和 js 调用栈关联的对象收集起来,作为三色标记的根对象。而后,从根对象作为标记入口,顺次递归标记子对象。遍历 gc_obj_list(quickjs 的所有须要 gc 的对象都在这个双向链表上),将没有被标记到的对象放入 tmp_obj_list。最初,开释 tmp_obj_list 中的对象。

单线程的 gc 会在 gc 过程中齐全暂停 js 的执行,存在潜在的业务卡顿危险(仅仅是潜在,因为理论业务的内存使用量较小,暂并未呈现由 gc 导致的卡顿),并且会让 js 的执行工夫绝对较长。为此 hygc 引入了多线程的三色标记,其流程如下:

在多线程版本中,存在 js 和 gc 两个线程,js 线程实现根对象收集及老对象转移到异步 gc 链表,而后 js 继续执行。gc 线程会先将老对象的三色标记全设为 0,而后开始标记存活对象,而后对垃圾对象进行收集。这里将垃圾对象的开释拆分成了 2 个阶段,一个是能够在 gc 线程执行的垃圾对象相干属性批改及置空,另一个是须要在 js 线程做的内存开释,这么做的起因是 hymalloc 不是线程平安的。这样 js 线程中的 gc 操作就只剩下绝对不耗时的根对象收集、老对象转移、内存开释三个操作。

注:令人悲伤的是,因为 mark 和垃圾回收依然只在独自一个线程实现,这里只用到了两种色彩做标记,灰色实际上没用到。后续优化让 hygc 实现和 quickjs 本来的 gc 可能共存,让 gc 的迁徙危险更低。

4)hygc 的局限性

hygc 的异步线程在做垃圾回收时,仅仅会对老对象做 gc,在实现老对象转移后的新对象将不会参加 gc,可能会造成内存应用峰值的晋升,晋升水平与 gc 线程的执行耗时相干。

此问题后续也将依据理论状况,判断是否进行计划优化来解决。

其余优化举例

1)global 对象的 inline cache

quickjs 的 global 对象的操作被独自编译为了 OP_get_var/OP_put_var 等 op,而这两个 op 的实现分外的慢,为此咱们对 global object 拜访加上了 inline cache。对 js 的对象属性拜访能够简化了解为在遍历数组来找到想要的属性,inline cache 的目标就是缓存住某段代码拜访的属性所在的数组中的偏移,这样下次取就间接用偏移来取了,不必再做反复的属性数组遍历。

global inline cache 的数据结构如下:

typedef struct {
    JSAtom prop;  // property atom
    int offset;   // cached property offset
    void *obj;    // global_obj or global_var_obj
} HYJSGlobalIC;
这里的第 4 行的 void *obj 比拟非凡,起因在于 quickjs 的 global 可能存在 context 对象的 global_obj 或 global_var_obj 中,具体存在哪个外面须要一并放入 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;
}

首先是第 5 行的 hyjs_GetGlobalICOffset,这个办法会为以后 opcode 调配一个 inline cache 的 cache_index,这个 cache_index 会在第 31 行设定为 HYJS_GetGlobalVar 办法调用的第 4 个参数。代码的第 9 行到第 19 行,会依据 cache_index 取 cache,并依据 cache 中的 offset,取 global 对象对应偏移里存的 prop(也就是属性 id,数据类型是 atom),和以后须要取的对象的属性的 atom 比拟,确认 cache 是否依然无效。如果 cache 无效则通过第 20-22 行代码间接取对象属性数组,如果有效则走到第 26 行的慢门路,遍历属性数组,并更新 inline cache。

2)builtin 的快门路优化

快门路优化是将代码中的某些执行概率更高的局部,独自提出来,来防止冗余代码的执行拖慢性能。

以 Array.indexOf 的实现为例:

static JSValue hyjs_array_indexOf(JSContext *ctx, JSValueConst func_obj,
                                  JSValueConst obj,
                                  int argc, 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;
                    goto done;
                }
            }
            goto property_path;
        }
        
        // slow path
        for (; n < count; n++) {if (js_strict_eq2(ctx, JS_DupValue(ctx, argv[0]),
                              JS_DupValue(ctx, arrp[n]), JS_EQ_STRICT)) {
                res = n;
                goto done;e
            }
        }
        
        ......
    }
 done:
    return JS_NewInt64(ctx, res);

 exception:
    return JS_EXCEPTION;
}

本来的实现是从第 23 行开始的慢门路,这里须要调用 js_strict_eq2 办法来判断数组 index 是否相等,这个比拟办法会绝对比拟重。而实际上 index 绝大多数状况都是 int 类型,所以提出来第 12 行的快门路,如果 index 自身是 int 类型,那么间接做 int 类型数据的比拟,就会比调用 js_strict_eq2 来比拟要快。

四 优化后果

性能测试设施基于 m1(arm64) 芯片的 macbook,wasm 业务性能测试基于 huawei mate 8 手机;测试后果抉择办法为每个 case 跑 5 次,取排第 3 位的后果;测试 case 抉择为斐波那契数列、benchmark、业务 case 三种,以评估不同场景下优化带来的性能变动。

1 wasm 性能

注:在业务 case 中得出的工夫是单帧渲染的整体耗时,包含 wasm 执行和渲染耗时两局部。

注:coremark hyengine jit 耗时是 llvm 编译版本的约 3 倍,起因在于对计算指令优化有余,后续可在优化器中对更多计算指令进行优化。

注:上述测试编译优化选项为 O3。

2 js 性能

注:microbench 的局部单项在 gc 优化上有负向的优化,使得整体优化的晋升并不显著,但改单项对业务影响不大。

注:从业务 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…
javascriptcore: https://opensource.apple.com/…
golang/arch: https://github.com/golang/arch
libmalloc: https://opensource.apple.com/…
Trash talk: the Orinoco garbage collector: https://v8.dev/blog/trash-talk
JavaScript engine fundamentals: Shapes and Inline Caches:https://mathiasbynens.be/note…
cs143: https://web.stanford.edu/clas…
C in ASM(ARM64):https://www.zhihu.com/column/…

原文链接
本文为阿里云原创内容,未经容许不得转载。

正文完
 0