Skip to Content

跟踪及时编译 / Tracing JIT

Tracing JIT Compilation
Tracing JIT Compilation
Table of Contents

跟踪及时编译(Tracing JIT Compilation) 是一种虚拟机在程序运行时用来优化程序执行性能的技术,实现方式是通过对频繁执行操作的线性序列的统计并将其编译为本地机器码,并且执行他们。 这与传统基于每个方法进行编译优化的 JIT 编译有着本质的不同。

JIT 概览

及时编译(Just-in-time compilation) 是一种在运行时将部分程序编译为机器码来提升程序执行速度的技术。一种对不同 JIT 编译技术分类的方法是通过他们编译的范围。 基于方法的 JIT 编译器每次将一个方法编译成机器码,而 Tracing JIT 编译器将经常执行的循环序列作为它们编译的单元。 Tracing JIT 编译器基于这样的一个假设,即,程序在某些比较热的循环(“hot loops”)花费执行的大部分时间,并且后续的循环迭代往往执行相同的代码路径。拥有 Tracing JIT 编译器的虚拟机通常除了一个 Tracing JIT 编译器他们还有要么一个解释器,或者一个基于方法的编译器这样混合模式的执行环境。

Tracing JIT 编译器的技术细

Tracing JIT 编译器 在运行时经过多个阶段。 首先,收集循环的 profiling 信息。在一个热循环被确认后,将进入一个记录着该循环所有执行操作的特殊的跟踪模式(tracing mode),将所有操作序列记为一条 trace。随后这条 trace 将被优化并编译成机器码(trace)。 当这个循环再一次被执行的时候,被调用的将会是编译过的 trace 而不是相关的程序。


相关各执行阶段解释如下:

分析(Profiling)阶段

Profiling 的目的在于识别出热循环(hot loops)。通常是通过统计每个循环的迭代次数来实现的,在一个循环执行的次数超过某些阈值,这个循环将被认为是热的,随机将进入 Tracing 模式。

跟踪(Tracing) 阶段

Tracing 阶段,循环正常执行的同时,每个执行的操作将会以一种中间表示(intermediate representation / IR code)的形式被记录到一条 trace中。 Tracing 跟随函数调用,所以函数也将被内联到 trace 中,Tracing 操作会一直进行直到循环的结束跳出回到循环开始。 由于 trace 根据一条条具体的循环执行路径记录,后续对该 trace 的执行可能出现分叉,即不再是之前的执行路径,为了识别这样分叉发生的地方,特殊的保护指令(guard instructions)被插入到 trace 中,一个典型的例子就是 if 语句。 保护指令就是对原始条件是否依然满足的一个快速的检查,如果检查失败,则当前 trace 的执行被终止。 因为 Tracing 过程是在执行阶段完成的,所以使得 trace 可以包含很多运行时的信息(比如:类型信息)。这些信息将被后面的优化阶段用来提升代码执行效率。

优化(Optimization)和 机器码生成(Code-Generation)阶段

Trace 是非常容易被优化的,因为他们仅仅表示仅仅一条执行路径,意味着没有控制流需要处理。典型的优化包括 常量子表达式消除(constant-subexpression elimination), 死代码消除(dead-code elimination), 寄存器分配(register allocation), 不变代码移动(invariant-code motion), 常量合并(constant folding), 和指针逃逸分析(escape analysis)等。

优化后, trace 将生成机器码,同优化阶段类似,生成机器码的过程很简单,因为 trace 的线性性质(linear nature)。

执行(Execution)

trace 被编译成机器码之后,将会被后续的循环迭代执行,直到保护指令检查失败。

Trace 实例

OpenResty 将 LuaJIT 嵌入 Nginx 中以获取 Lua 高效的开发以及 LuaJIT 与 Nginx 组合带来的高性能,微博开源跨语言 RPC 框架 Motan-OpenResty 基于此开发,下面我们就从 Motan-OpenResty 中来列举几个 Trace 的实例。

GDB 进入 Motan-OpenResty Debug 模式:

执行 ltrace 命令获取当前被 JIT 编译的所有 trace 条数。

gdb ltrace
Found 45 traces.

执行 ltrace number ltrace 命令后加上对应编号,将获取该条 trace 对象的详细信息,下面我们就来详细分析下第一条 trace。

gdb ltrace 1
(GCtrace*)0xb51510
machine code size: 242
machine code start addr: 0x7ffff7fe7f07
machine code end addr: 0x7ffff7fe7ff9
@/media/psf/g/idevz/code/www/vanilla/framework/motan/config/parse.lua:9

我们来看看这部分被 LuaJIT 编译优化过的代码(节选: Motan-OpenResty/config/parse.lua:9):

-- Copyright (C) idevz (idevz.org)


local _M = {}

function _M.ini(lines, name)
    local t = {}
    local section
    for line in lines(name) do
        local s = line:match("^%[([^%]]+)%]$")
        if s then
            section = s
            t[section] = t[section] or {}
            goto CONTINUE
        end

Motan-OpenResty 在启动的时候,第一步是进行一系列的配置解析和加载,我们自己实现了 ini 格式的配置加载,第一条被 LuaJIT 选中的就是这个 parse 操作的第一个 for 循环,这必须是第一条最热的代码路径。

上面 ltrace 1 命令输出的机器码开始地址和结束地址可以用来获取当前 trace 生成的机器码,如下(节选部分):

gdb set disassembly-flavor intel
gdb disas 0x7ffff7fe7f07,0x7ffff7fe7ff9
Dump of assembler code from 0x7ffff7fe7f07 to 0x7ffff7fe7ff9:
   0x00007ffff7fe7f07:	mov    DWORD PTR ds:0xb1be68,0x1
   0x00007ffff7fe7f12:	cmp    DWORD PTR [rdx+0x3c],0xfffffffb
   0x00007ffff7fe7f16:	jne    0x7ffff7fd8010
   
   ... ...

   0x00007ffff7fe7fe4:	mov    eax,0x3
   0x00007ffff7fe7fe9:	mov    ebx,0xb1bf00
   0x00007ffff7fe7fee:	mov    r14d,0xb1ca38
   0x00007ffff7fe7ff4:	jmp    0x7ffff74fa4c2 <lj_vm_exit_interp>
End of assembler dump.

我们也可以通过 lir 命令来获取 trace JIT 过程中的中间表示码(intermediate representation / IR code)

gdb lir 1
(GCtrace*)0xb51510
IR count: 13

---- TRACE 1 start parse.lua:9
---- TRACE 1 IR
....              SNAP   #0   [ ---- ---- ---- ---- ---- ---- ---- ---- ---- ]
0001 rbp   >  str SLOAD  #8    T
0002          int FLOAD  0xb21fb0  tab.hmask
0003       >  int EQ     0002  +1
0004 rbx      p32 FLOAD  0xb21fb0  tab.node
0005       >  p32 HREFK  0004  "__index"
0006 rbx   >  tab HLOAD  0005
0007          int FLOAD  0006  tab.hmask
0008       >  int EQ     0007  +15
0009 rbx      p32 FLOAD  0006  tab.node
0010       >  p32 HREFK  0009  "match" @3
0011       >  fun HLOAD  0010
0012       >  fun EQ     0011  string.gmatch
....              SNAP   #1   [ ---- ---- ---- ---- ---- ---- ---- ---- ---- 0xb51510 [0x00003348] string.gmatch|0001 "^\%[\([^\%]]+\)\%]\$" ]

至此,我们应该对 Trace JIT 编译技术,对 LuaJIT 有个整体的了解了。

https://en.wikipedia.org/wiki/Tracing_just-in-time_compilation

微信公众号 / Vanilla-OpenResty