做了很多自己的理解和整理,原文在这里,建议直接读原文,更香 https:// hacks.mozilla.org/2017/ 02/a-crash-course-in-just-in-time-jit-compilers/
As we all know,我们写的代码是要被翻译成机器码才能被机器执行的。
大体来说,有两种方式可以将程序翻译成机器可执行的指令,使用编译器 (Compiler) 或者是 解释器 (Interpreter)。
解释器
解释器是边翻译,边执行。
优缺点
- 优点:快速执行,不需要等待编译
- 缺点:相同的代码可能被翻译多次,比如循环内部的代码
![5d3de46413a5aad3bf48395ce8734096.png](https://i-blog.csdnimg.cn/blog_migrate/a27bce414ebf47e9f02a26fda0375eed.jpeg)
编译器
而编译器则是提前将结果翻译出来,并生成一个可执行程序。 优缺点
- 优点:不需要重复编译,并且可以在编译时对代码做优化
- 缺点:需要提前编译
![f100bf4b1756c2135e380c94b15be7ed.png](https://i-blog.csdnimg.cn/blog_migrate/3fdfaa62cd7a6ef5d1709fd4914d701b.jpeg)
JIT
JavaScript 刚出现的时候,是一个典型的解释型语言,因此运行速度极慢,后来浏览器引入了 JIT compiler,大幅提高了 JavaScript 的运行速度。
原理:They added a new part to the JavaScript engine, called a monitor (aka a profiler). That monitor watches the code as it runs, and makes a note of how many times it is run and what types are used.
简单来说,浏览器在 JavaScript engine 中加入了一个 monitor,用来观察运行的代码。并记录下每段代码运行的次数和代码中的变量的类型。
那么问题来了,为什么这样做能提高运行速度?
后面的所有内容都以下面这个函数的运行为例
function arraySum(arr) {
var sum = 0;
for (var i = 0; i < arr.length; i++) {
sum += arr[i];
}
}
1st step - Interpreter
一开始只是简单的使用解释器执行,当某一行代码被执行了几次,这行代码会被打上 Warm 的标签;当某一行代码被执行了很多次,这行代码会被打上 Hot 的标签
![67a01af5778a3c70202e9dd1d113b5a5.png](https://i-blog.csdnimg.cn/blog_migrate/1328635e88a45cc74d1e310a98b6fe53.jpeg)
2nd step - Baseline compiler
被打上 Warm 标签的代码会被传给 Baseline Compiler 编译且储存,同时按照行数 (Line number) 和变量类型 (Variable type) 被索引(为什么会引入变量类型做索引很重要,后面会讲)
当发现执行的代码命中索引,会直接取出编译后的代码执行,从而不需要重复编译已经编译过的代码
![3e91d010a1f6004415127ad413f3eae1.png](https://i-blog.csdnimg.cn/blog_migrate/7c47af92155dfc09b42451d1c9fcc46f.jpeg)
3rd step - Optimizing compiler
被打上 Hot 标签的代码会被传给 Optimizing compiler,这里会对这部分带码做更优化的编译。怎么样做更优化的编译呢?关键点就在这里,没有别的办法,只能用概率模型做一些合理的 ”假设 (Assumptions)“。
![e119ff621a45e14fa75f1f36eb1ca4b3.png](https://i-blog.csdnimg.cn/blog_migrate/896f7498f8825d2d75bd7ad417dc4589.jpeg)
比如我们上面的循环中的代码 sum += arr[i]
,尽管这里只是简单的 +
运算和赋值,但是因为 JavaScript 的动态类型 (Dynamic typing),对应的编译结果有很多种可能(这个角度能很明显的暴露动态类型的缺点)
比如
- sum 是 Int,arr 是 Array,i 是 Int,这里的
+
就是加法运算,对应其中一种编译结果 - sum 是 string,arr 是 Array,i 是 Int,这里的
+
就是字符串拼接,并且需要把 i 转换为 string 类型 - ...
下面的图可以看出,这么简单的一行代码对应有 2^4 = 16 种可能的编译结果
![7d2fe489d0e23ecce3d21175c1042fba.png](https://i-blog.csdnimg.cn/blog_migrate/27f550694074dd43c9897cfba48bfbf7.jpeg)
前面第二步的 Baseline compiler 做的就是这件事,所以上面说编译后的代码需要使用 line number 和 variable type 一起做索引,因为不同的 variable type 对应不同的编译结果。
如果代码是 "Warm" 的,JIT 的任务也就到此为止,后面每次执行的时候,需要先判断类型,再使用对应类型的编译结果就好。
![b4f7d151a32f26564c13fc7c8d5ca937.png](https://i-blog.csdnimg.cn/blog_migrate/ea2f4ff40bf6713c6b58cb806b107ae4.jpeg)
但是上面我们说,当代码变成 "hot" 的时候,会做更多的优化。这里的优化其实指的就是 JIT 直接假设一个前提,比如这里我们直接假设 sum 是 Int,i 也是 Int,arr 是 Array,于是就只用一种编译结果就好了。
![96820771295a023c68c8e9db950ca0f4.png](https://i-blog.csdnimg.cn/blog_migrate/71e404b7286026f2ba1b80b09741f619.jpeg)
实际上,在执行前会做类型检查,看是假设是否成立,如果不成立执行就会被打回 interpreter 或者 baseline compiler 的版本,这个操作叫做 "反优化 (deoptimization)"。
可以看出,只要假设的成功率足够高,那么代码的执行速度就会快。但是如果假设的成功率很低,那么会导致比没有任何优化的时候还要慢(因为要经历 optimize => deoptimize 的过程)
![8adcdb287a0bdbc9c8ada044583540c0.png](https://i-blog.csdnimg.cn/blog_migrate/3f074f99a956549989702e6308cb3018.jpeg)
这里就引申出两个问题,
- 如何做合理的假设?
- 假设失败率很高的时候怎么处理?
这里作者都有做解答,但是我觉得这不是重点我就直接把回答 copy 过来好了
Answer 1: The optimizing compiler uses the information the monitor has gathered by watching code execution to make these judgments. If something has been true for all previous passes through a loop, it assumes it will continue to be true.
Answer 2: Most browsers have added limits to break out of these optimization/deoptimization cycles when they happen. If the JIT has made more than, say, 10 attempts at optimizing and keeps having to throw it out, it will just stop trying
总结
JavaScript 运行速度的提升离不开 JIT compiler 的贡献,通过对多次执行的代码的编译结果的存储,以及对变量类型的合理推测,尽管存在运行时间加长的可能,但还是整体降低了 JavaScript 代码的平均执行时间。
这对我们写 JavaScript 有什么帮助呢,其实帮助不大,只是说提醒我们保持一个很好的习惯就是不要随意修改一个变量的类型。
真正震撼到我的点,不是了解了 JIT 是怎么工作的,而是 JIT 的原理竟然是 “概率”
从架构层面来讲,其实我们做任何性能优化,都要先问下目标是是平均值,还是极值。我相信大多数时候需要优化的指标都是平均值,那平均值比极值多出来一个维度那就是概率分布。对 100% 场景下的 1 秒优化可能远远比对 80% 场景下的 3s 优化还要难的多,但是收益却是后者高。
从代码层面来说,我们大多数时候写代码结果都是 100% 确定的,如果有一定概率产生不一样的结果,那叫 bug。但如果你加快了 90% 情况下的执行速度,同时出了 10% 概率的 "bug",并且 catch 了 这 10% 的错误做了额外的处理,最后的整体表现说不定会更好。
这种思维方式的引入说不定会碰出不一样的火花