Just-In-Time (JIT) Compilers
Introduction
Compiler & Interpreter 这篇介绍了 Compiler 和 Interpreter 以及它们各自的优缺点,我们先来简单回顾一下。
Compiler: 事先把源码编译成机器语言,在需要执行程序的时候直接运行编译后的代码。因为编译工作是事先完成的,所以 Compiler 在编译的时候有充足的时间好好分析源码,并对其进行优化,生成运行速度更快的代码。
Interpreter:在需要执行程序的时候才开始边编译,边运行编译后的代码。如果是在运行一个循环语句,那代码每次循环都要重新编译一次。而且由于是一边编译一边运行,时间很紧,Interpreter 也不能在编译代码的过程中做太多优化的工作,生成的代码运行速度相对慢一些。
而 JIT 则是结合了 Compiler 和 Interpreter 的优点,是两者的一个完美结合。
How JIT Works
在程序刚开始运行的时候,JIT 像 Interpreter 一样一行一行地编译并运行代码;
在这个过程中,如果它发现了一些代码需要重复地运行(比如循环中的代码),就会像 Compiler 一样把这部分代码进行编译并存起来,下次再需要运行这部分代码时,就从内存中取出编译后的代码直接运行;
How JIT Compiles Codes
如果 JIT 发现了需要多次运行的代码,它会把这部分代码进行编译,但编译的方式分两种,分别由 Baseline Compiler 和 Optimizing Compiler 来负责。
Baseline Compiler
Baseline Compiler 负责做一些简单的编译工作,编译时间不长,但生成的代码优化程度也不高。如果把代码优化的过程比喻成论文修改的话,Baseline Compiler 就只是改改标点符号和错别字而已。
Optimizing Compiler
Optimizing Compiler 负责将源代码编译成优化程度更高的代码,编译时间较长,不过生成的代码运行速度更快,相当于对论文的结构和内容进行了优化。
How JIT Knows Which To Use
那 JIT 怎么知道一行代码是否需要优化?
在代码执行的时候,JS 引擎会用一个 monitor
(aka. profiler
) 来监测并记录代码的执行频率。
如果一行代码被重复执行了几次,
monitor
会将它标记为warm
,JIT 就会把它送到 Baseline Compiler 去编译,然后把编译后的代码存起来。如果一行代码被重复执行了很多很多次,它就会被标记为
hot
,JIT 就会把它送到 Optimizing Compiler 去编译,然后把编译后的代码存起来。
Extension
通过监测代码执行频率以及编译优化代码,JIT 提高了 JS 的运行速度,不过它同时也带来了一些 overhead:
优化和反优化(optimization and deoptimization)
上文没有提到的是,Compiler 在优化代码的时候会作出一些假设。
比如说函数
function foo(a) {}
由于执行较频繁被送到了 Optimizing Compiler 去优化;前提:
foo
在最初几次执行的时候接收到的参数a
都是字符串类型的;Compiler 会假设在之后的调用中,传入的
a
也都是字符串,并基于这个假设对foo
进行优化;如果程序之后调用
foo
时都是传入字符串,那一切就都很美好;不过,假如有一次调用中传入了数字,因为对数字和字符串的操作不一样,之前基于假设
a 是字符串
优化的代码就完全用不上了,这时 Compiler 就会将代码反优化并重新进行编译和优化;假如在程序运行中 Compiler 做了很多这样错误的假设,一直在优化和反优化同一段代码的话,有可能消耗的时间比运行没有优化的代码还长;
不过,浏览器对此也做了一些处理,如果优化和反优化这个过程重复了太多次的话,就直接放弃了对这段代码进行优化。
额外的内存占用
monitor 记录的代码执行频率等信息需要占用一部分内存
编译后的代码也需要地方储存起来
Last updated