在动态语言中,动态类型代码 JIT 是如何编译成机器码的?更具体地说:编译器是否在某个时候推断出类型?还是在这些情况下严格解释?
例如,如果我有类似下面的伪代码
def func(arg)
if (arg)
return 6
else
return "Hi"
执行平台如何在运行代码之前知道函数的返回类型是什么?
在动态语言中,动态类型代码 JIT 是如何编译成机器码的?更具体地说:编译器是否在某个时候推断出类型?还是在这些情况下严格解释?
例如,如果我有类似下面的伪代码
def func(arg)
if (arg)
return 6
else
return "Hi"
执行平台如何在运行代码之前知道函数的返回类型是什么?
一般来说,它不会。但是,它可以采用任何一种类型,并为此进行优化。细节取决于它是哪种 JIT。
所谓跟踪JIT编译器对程序进行解释和观察,并记录单次运行的类型、分支等(例如循环迭代)。他们记录这些观察结果,插入(非常快)检查这些假设在执行代码时是否仍然正确,然后根据这些假设优化以下代码。例如,如果你的函数在一个循环中调用,并添加一个持续为真的参数,JIT 编译器首先会记录这样的指令(我们将忽略调用帧管理、内存分配、变量间接等。不是因为那些不重要,但因为它们占用了大量代码并且也被优化了):
; calculate arg
guard_true(arg)
boxed_object o1 = box_int(6)
guard_is_boxed_int(o1)
int i1 = unbox_int(o1)
int i2 = 1
i3 = add_int(res2, res3)
然后像这样优化它:
; calculate arg
; may even be elided, arg may be constant without you realizing it
guard_true(arg)
; guard_is_boxed_int constant-folded away
; unbox_int constant-folded away
; add_int constant-folded away
int i3 = 7
防护也可以移动以允许优化早期代码,合并以减少防护,如果它们是多余的则省略,加强以允许更多优化等。如果防护失败过于频繁,或者某些代码以其他方式变得无用,则可以将其丢弃,或者至少修补以在防护失败时跳转到不同的版本。
其他 JIT 采用更静态的方法。例如,您可以进行快速、不准确的类型推断,以至少识别一些操作。一些 JIT 编译器仅在函数范围内运行(因此它们被某些人称为方法 JIT 编译器),因此它们可能无法充分利用您的代码片段(跟踪 JIT 编译器非常受欢迎的一个原因)。尽管如此,它们仍然存在——一个例子是 Mozilla 的 JavaScript 引擎Ion Monkey的最新版本,尽管它显然也从跟踪 JIT 中获得了灵感。您还可以插入添加并非总是有效的优化(例如内联可能稍后更改的函数)并在它们出错时将其删除。
当所有其他方法都失败时,您可以做解释器所做的事情,将对象装箱,使用指向它们的指针,标记数据,并根据标记选择代码。但这是非常低效的,JIT 编译器的全部目的是摆脱这种开销,所以它们只会在没有合理的替代方案时(或者它们仍在预热时)这样做。