Bare Bones 后端

Bare Bones 后端,简称 B3,是 WebKit 针对包含 C 语言风格代码的过程(procedure)所使用的优化 JIT。它目前是 JavaScriptCore 内部 FTL JIT 的默认后端。

B3 包含一个被称为“B3 IR”的C 语言风格 SSA IR,对 B3 IR 的优化,一个被称为“Air”的汇编 IR,对 Air 的优化,一个将 B3 IR 转换为 Air 的指令选择器,以及一个将 Air 组装成机器码的代码生成器。

你好,世界!

这是一个简单的 C++ 代码示例,它使用 B3 生成一个将参数加二并返回的函数

// Create a Procedure that holds our code.
Procedure proc;
BasicBlock* root = proc.addBlock();
root->appendNew<Value>(
    proc, Return, Origin(),
    root->appendNew<Value>(
        proc, Add, Origin(),
        root->appendNew(proc, Origin(), GPRInfo::argumentGPR0),
        root->appendNew(proc, Origin(), 2)));

// Have B3 compile the Procedure into code. The code and all of its artifacts (constant pools, jump tables)
// will stay alive so long as Compilation stays alive.
std::unique_ptr<Compilation> compilation = std::make_unique<Compilation>(vm, proc);

// Get a function pointer that we can call. This function pointer points to JIT-generated machine code.
int64_t (*function)(int64_t) = bitwise_cast<int64_t (*)(int64_t)>(compilation->code().executableAddress());

// Run it and print the result!
printf("%lld\n", function(42)); // Prints 44.

编译后,生成的机器码如下所示

0x3aa6eb801000: pushq %rbp
0x3aa6eb801001: movq %rsp, %rbp
0x3aa6eb801004: leaq 0x2(%rdi), %rax
0x3aa6eb801008: popq %rbp
0x3aa6eb801009: ret 

B3 总是发出帧指针序言/尾声,以便与调试工具良好协作。除此之外,您可以看到 B3 将程序的本体优化为单个指令:在本例中,是一个加载有效地址(Load Effective Address)指令,用于将 %rdi + 2(其中 %rdi 是第一个参数寄存器)传输到 %rax(即结果寄存器)。

B3 IR

B3 的客户端通常通过使用B3 IR与它交互。它是 C 语言风格的,因为它将堆引用建模为整数,并且不尝试验证内存访问。它强制执行静态单赋值(static single assignment),简称 SSA。一个 SSA 程序对每个变量只包含一次赋值,这使得从变量的使用追溯到定义其值的操作变得轻而易举。B3 IR 的设计目标是易于生成且操作成本低廉。

在大多数方面,B3 IR 是平台无关的。然而,由于 B3 被设计用作 JIT 的后端,因此在可行时,它确实包含了一些特定于平台的概念。B3 提供了对参数寄存器、堆栈帧布局、帧指针和调用参数区域的直接控制。它能够发出定义全新调用约定(无论是针对正在生成的程序调用者,还是程序调用点的被调用者)的 B3 IR。B3 也使得发出 C 语言风格的调用变得容易。为此有一个专门的操作码。

更多信息请参阅IR 文档

以下是上述示例的 IR 示例

BB#0: ; frequency = 1.000000
    Int64 @0 = ArgumentReg(%rdi)
    Int64 @1 = Const64(2)
    Int64 @2 = Add(@0, $2(@1))
    Void @3 = Return(@2, Terminal)

B3 优化

B3 相当新——我们直到 2015 年 10 月末才开始着手开发它。但它已经拥有一些出色的优化功能:

Air

Air,即汇编 IR,是 B3 在代码生成之前表示机器指令序列的方式。Air 类似于汇编,但除了寄存器之外,它还有临时变量;除了原生地址形式之外,它还有抽象形式,例如“stack”(一个抽象的堆栈槽)和“callArg”(堆栈中传出调用参数区域的一个抽象位置)。

以下是上述示例生成的初始 Air

BB#0: ; frequency = 1.000000
    Move %rdi, %tmp1, @0
    Move $2, %tmp2, $2(@1)
    Add64 $2, %tmp1, %tmp0, @2
    Move %tmp0, %rax, @3
    Ret64 %rax, @3

请注意,“@”引用表示该指令在 B3 IR 中的来源。

Air 优化

Air 具有复杂的优化功能,可以将使用临时变量和抽象堆栈位置的程序转换为直接使用寄存器的程序。Air 还负责处理与 ABI 相关的问题,例如堆栈布局和 C 语言调用约定。Air 具有以下优化:

以下是这些优化对示例程序所做的工作

BB#0: ; frequency = 1.000000
    Add64 $2, %rdi, %rax, @2
    Ret64 %rax, @3

B3 到 Air 的降低,也称为指令选择

B3::LowerToAir 阶段通过模式匹配将 B3 转换为 Air。它反向处理程序。在每个 B3 值处,它贪婪地尝试匹配该值及其尽可能多的子项(即它使用的值)以及它们的子项,以创建一个单独的指令。不同的硬件目标支持不同的指令。Air 允许 B3 引用所有目标上所有指令的超集,但提供了一个快速查询来检查给定的指令或特定的指令形式(例如三操作数加法)是否可用。指令选择器简单地遍历它所知道的模式,直到找到一个能在 Air 中产生合法指令的模式。

指令选择器功能强大,足以执行比较分支(compare-branch)和加载-操作-存储融合(load-op-store fusion)等基本操作。它足够智能,可以执行我们称之为“超级组合技”(Mega Combo)的操作,其中以下 B3 IR

Int64 @0 = ArgumentReg(%rdi)
Int64 @1 = ArgumentReg(%rsi)
Int32 @2 = Trunc(@1)
Int64 @3 = ZExt32(@2)
Int32 @4 = Const32(1)
Int64 @5 = Shl(@3, $1(@4))
Int64 @6 = Add(@0, @5)
Int32 @7 = Load8S(@6, ControlDependent|Reads:Top)
Int32 @8 = Const32(42)
Int32 @9 = LessThan(@7, $42(@8))
Void @10 = Check(@9:WarmAny, generator = 0x103fe1010, earlyClobbered = [], lateClobbered = [],
                 usedRegisters = [], ExitsSideways|Reads:Top)

被转换为以下 Air

Move %rsi, %tmp7, @1
Move %rdi, %tmp1, @0
Move32 %tmp7, %tmp2, @3
Patch &Branch8(3,SameAsRep), LessThan, (%tmp1,%tmp2,2), $42, @10

最终生成的代码是

0x311001401004: movl %esi, %eax
0x311001401006: cmpb $0x2a, (%rdi,%rax,2)
0x31100140100a: jl 0x311001401015

除了用于处理用作索引的 32 位参数的强制零扩展操作外,B3 足够智能,可以将地址计算、加载和比较转换为单个指令,然后将其与分支融合。

代码生成

Air 的最终形式不包含未分配的临时变量或抽象堆栈槽。因此,它直接映射到机器码。最终的代码生成步骤是一个非常快的转换过程,将 Air 表示这些指令的面向对象方式转换为目标的机器码。我们为此使用了 JavaScriptCore 的宏汇编器。