JIT 真的比解释执行快么 —— 关于 JS 引擎的一些热门话题
在编程语言的世界中,如何高效地执行代码一直是一个热门话题。随着脚本语言的普及和性能需求的提升,解释执行和即时编译(JIT)成为了两种常见的代码执行方式。本文探讨了这两种技术,通过详细的实例和深入的分析,为我们揭示了它们的工作原理、性能差异以及各自的优缺点。
希望这篇文章能够帮助你更好地理解编程语言执行的技术世界,激发你对高效代码执行的深入思考,并在实践中应用这些宝贵的知识。
要解释什么是JIT,什么是解释执行,我们来看一个简单的例子,就很好理解了:
语言
,一定有一套规定好的行为。执行这个 语言
编写的程序,就是按照规定好的行为一行一行逐步生效的过程。C语言有这样一套规定,比如 a=b+c
; 就代表了: -
取出变量b内存中的数字 -
取出变量c内存中的数字 -
相加 -
结果放到变量a里
C语言的“规定”本身比较简单,由于强类型,其行为上也贴近机器码的行为。我们通过编译把C代码转换成机器码后,汇编代码和C代码之间的对应关系还是比较清晰的。
a = b + c
举例,在JS中,规定可能是这样的: -
取出变量b内存中的数字
-
变量b是一个闭包, 那么要xxxx -
变量b如果是一个局部变量, 那么要xxxx -
变量b如果是一个全局变量, 那么要xxxx -
变量b如果不存在, 那么要xxxx -
....
-
取出变量c内存中的数字
-
.... (同上)
-
相加
-
相加的值如果都是数字类型, 那么xxxx -
相加的值如果都是字符串类型,那么xxxxx -
相加的值如果是xxxxx, 那么xxxxx -
....
-
(如果相加过程抛出了异常)
-
如果异常有catch block, 那么xxxxx -
如果异常没有catch block,那么xxxxx
-
结果放到变量a里
-
变量b是一个闭包, 那么要xxxx -
变量b如果是一个局部变量, 那么要xxxx -
变量b如果是一个全局变量, 那么要xxxx -
变量b如果不存在, 那么要xxxx -
....
ToInt16
行为,按照规范有1,2,3,4,5多个步骤,步骤中又会用到一些其他步骤,比如 ToNumber
。 函数
,规范中行为的步骤就变成了 函数
的一行行代码,然后 函数
和 函数
之间的调用就可以实现这么复杂的语言规范组织了。我们把这些逻辑叫做 步骤函数
步骤函数
的概念来理解: -
解释执行 : 把JS脚本转换成一个步骤函数数组存到内存里,然后执行时写一个大的while循环,把步骤函数的地址取出来,然后调用这个函数指针。
Func array[] = {doVarGet, doVarGet, doAdd, doVarPut }
void interpretor(VM* vm) {
while(*array) { // 一步步执行所有步骤
(*array)(vm); // 执行当前步骤
array++; // 准备执行下一个步骤
}
}
-
JIT : 把一个个步骤函数的调用转换成机器码,不用软件的while来驱动,可以理解成这样的C代码
void jit(VM* vm) {
doVarGet(vm);
doVarGet(vm);
doAdd(vm);
doVarPut(vm);
}
从上面的概念上看来不管怎么样都应该是JIT的方式比解释执行的方式快,那么为什么还存在解释执行这种方式呢?有以下几个原因:
-
JIT生成的
代码
体积会比array数组
(在真实情况下一般是字节码) 大很多,内存消耗太大。 -
JIT需要运行时支持将内存页的权限标记为
"executable"
的,这在一些系统上是做不到的:比如IOS和鸿蒙出于安全原因禁止这种行为 -
对于
步骤函数
比较复杂的语言,while循环
往下走一个循环的开销可能比起doVarGet
来说是微不足道的,这样 JIT 比解释执行也快不了多少,甚至由于过大的可执行内存段,会经常造成L1 Cache miss
拖慢整体执行。这点下文会看到一个通过CPython实现Baseline JIT的例子。
另外,JIT(Just-In-Time)
是和 AOT (Ahead-Of-Time)
相对应的概念。这种从JS源码转换成机器码的过程是在运行时动态进行的,而不是像C语言一样预编译好的。对于很多只要执行一次的代码,进行JIT编译的开销加上执行JIT后代码的耗时,可能比直接用解释执行执行这些代码的要慢。
解释执行为什么慢
br
跳转指令,后面跟的地址是一个动态的地址。这些特点产生了如下问题: -
if else
和br
指令都会造成CPU流水线失效,不能有效的利用指令并行的能力。
当我们认识到解释执行为什么慢以后,自然而然走到了我们的第二个“热门话题”: “JIT真的超快的呢=w=”。知其然知其所以然,我们这里要讲一下JIT执行到底快在哪里。
-
JIT的语言本身要有静态的类型信息,不然翻译出来的代码又要有很多的if else类型判断,拖慢产物的执行性能。 -
JIT的设计上要能进行和静态编译类似的各类分析优化,比如公共子表达式擦除,数据依赖分析,数据逃逸分析等等。 -
VM的设计上要能很好的收集运行时的数据,来决定什么是热点,决定那些函数JIT化,哪些函数在JIT中inline等等。
比如Java/JS这样的语言是支持NullPointerException
的,遇到a.b中a是null/undefined
的情况,是可以在当场抛出Exception中断执行流程,然后外面可以通过catch这个Exception来防止整个程序崩溃的。
但是对于C/汇编来说,访问一个空指针对象的字段时,是会直接触发sigfault
信号造成程序崩溃的。那么我们怎么在JIT的代码中实现Exception呢?
对于解释执行来说,“步骤函数”: GetProperty
中会增加一个if判断来专门处理这种情况。但是对于生成jit代码呢?难道也每个属性访问前面都if else?那不是和解释执行一样慢了么?
对于V8/JVM来说,他的实现方式是:
-
JIT代码中不生成空指针检查的代码,让他触发
sigfault
-
引擎监听linux系统的
sigfault
信号,然后根据信号触发时的地址偏移位置,反向推断出当前是哪一行代码出现了问题,出现了什么问题。 -
根据(2)中的计算结果,进行 de-optimize , 也就是重新回到解释执行模式中,让解释执行来触发Exception并处理。
那么怎么知道回到解释执行中的哪一个代码位置,回到这个位置有多少函数的状态是需要还原的,其中要有非常复杂的逻辑要处理。大概的类比就是实现一套 dwarf 功能 (C代码编译的调试符号文件)。
那么V8这种在JS这种动态语言上做JIT的引擎,其实比Java做JIT会更加困难,因为Java的输入起码是固定类型的,数字类型的 b+c
真的可以生成对应的机器码来执行。V8的想要做到类似的效果,需要在运行时动态地去收集运行时类型信息,比如一个函数中的 b+c
一直都是数字类型的,那么他可能就假设这个函数大概率是数字类型的,然后按照数字类型去生成一段JIT。后续在使用的时候要检查前置条件 (b和c都是数字类型) 来决定是否可以使用这段JIT代码。
至此我们可以看到,JIT真的是可以做到非常快的执行的,在一些特定类型固定的函数上甚至可以生成和 C 代码编译结果一样质量的JIT函数的。但是其中 VM 要做到工作是非常非常复杂的。不妨看一下业界已有的一些带有JIT功能的语言引擎:
-
Java/C#: Oracle和微软维护
-
PHP: 社区版本没有JIT,Facebook自己做了一个带JIT的版本维护。
-
V8/JSC: 谷歌和苹果维护
然后我们再看一下一些社区维护的语言:
-
Python: 官方的CPython至今没有一个完善的JIT实现。PyPy可以实现JIT但是工业实践上很少使用。
-
Lua: 官方不包含JIT。LuaJIT版本是一个大佬自己维护的,而且已经不再更新Lua最新版本的支持了。
asm.js
技术。 asm.js
技术的思路很有意思,不妨在这里讲一下。 asm.js
编译器变成JS代码的例子,大家可以看到在这个JS产物里多了很多 |0
这样的奇怪代码,这其实是给JS引擎的JIT一个提示,代表了 (curr+1) | 0
的结果一定是 数字类型
。当JS引擎在一个确定的类型下进行JIT编译时,JIT产物的确定性和效率会好很多。这点也符合我们之前JIT的部分讲的,动态类型语言的JIT要在运行时收集类型信息,但是如果类型信息能在语法解析时就确定下来,那么对于JIT编译是一个极大的利好。 asm.js
产生以后,创造了一些在当年看起来仿佛神迹一般的效果,比如把 3D 游戏引擎在浏览器上高效的运行起来。顺着这个思路,几家浏览厂商说,既然要让JIT做得舒服,干嘛要JS进来插一脚,我们搞一个强类型的中间语言是不是比用JS更高效? ![]()
WASM真的快么
-
对于可以都做JIT的场景,WASM不一定能比JS快很多,必然JS在JIT做得够好的时候也可以很接近静态编译的效果。 -
但是WASM想要把JIT做好比JS要容易太多了。就拿de-optimize的事情来说,WASM里空指针是真的可以让WASM引擎进入不可恢复的状态的。这比起JS还要还原代码位置做退优化不知道简单了多少倍。 -
对于都不可做JIT的场景,WASM比JS快一点但不多。大家都是解释执行,不会有太大本质性差别。 -
对于WASM要嵌入到JS里,还要通过JS来访问各种外部API时,WASM和JS之间的通信成本甚至会导致严重的性能瓶颈。
CPython的JIT实现例子
void jit(VM* vm) {
doVarGet(vm);
doVarGet(vm);
doAdd(vm);
doVarPut(vm);
}
LuaJIT的实现例子
LuaJIT的实现中包含了一个典型的动态语言引擎做JIT所需要的各类手段, 可以从作者的这封邮件中看到:
http://lua-users.org/lists/lua-l/2009-11/msg00089.html
LuaJIT相比起CPython copy-and-patch的JIT实现明显正规的多。包含了IR,优化,退优化,寄存器分配等关键概念。想要自己实现JIT,LuaJIT应该是一个合适的参考对象。
解释执行真的慢么
-
现代编译器是为 "普通"的软件设计的,不能很好的处理 Interpretor 这种特殊软件。具体表现在:
-
不能很好的分配寄存器。 -
不能很好的区分解释器中的fast-path 和 slow-path。造成流水线失效。
Low-Level Interpreter
" 的文章中, 也提到了类似的观点: https://wingolog.org/archives/2012/06/27/inside-javascriptcores-low-level-interpreter -
根据calling-convention,函数的参数总是会使用寄存器。这确保了寄存器分配的确定性。 -
编译器的尾递归优化会重用当前栈,使得尾递归调用的执行效果就是进行了一次没有sp操作的跳转。这模拟了解释器的while-switch循环行为。 -
clang高版本的 [must-tail]
标记可以强制使用尾递归来优化,确保了编译产物的确定性。
![]()
端上业务是否需要JIT
three.js
, 比如一些矩阵计算代码,那么有 JIT 和没 JIT 的差别就很大了。此时最好还是选用支持JIT的引擎。 扩展1:动态语言是否可以AOT
-
JVM: 在JVM上支持了Java, Kotlin, Groovy等多种语言。 -
WASM: C Rust等静态编译语言都可以生成WASM。
扩展3:GraalVM是怎么实现多语言引擎的
不熟悉GraalVM的同学可以看下这个介绍。这其实就是一个实验性的新的JVM,设计上具有更好的JIT能力来取代现有的JVM。但是GraalVM上有一个神奇的功能叫Truffle,他可以在JVM上实现各种动态语言比如Ruby,比如Python,比如JS。然后这些语言都可以JIT后以比肩V8这种原生引擎的效率来执行。同时又保持了和Java良好的可互操作性(毕竟运行在Java的VM上)。
AST树
,然后在 AST树
上增加了求值函数?没错GraalVM实现语言的方式就是,你不需要定义自己的字节码,你只需要用Java实现AST和AST的求值方式就可以。 扩展4:汇编是否解决问题
tail-call
模式的优化,其实已经可以解决绝大多数汇编需要解决的问题了,而且有更好的可维护性。那么不妨先把这部分做了再来观察编译器汇编的结果。再看有没有优化空间。 简历投递邮箱: youyang.xyy@taobao.com
本文分享自微信公众号 - 大淘宝技术(AlibabaMTT)。
如有侵权,请联系 support@oschina.cn 删除。
本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一起分享。

低调大师中文资讯倾力打造互联网数据资讯、行业资源、电子商务、移动互联网、网络营销平台。
持续更新报道IT业界、互联网、市场资讯、驱动更新,是最及时权威的产业资讯及硬件资讯报道平台。
转载内容版权归作者及来源网站所有,本站原创内容转载请注明来源。
- 上一篇
树莓派与 Hailo 合作推出 AI 套件,售价 70 美元
Raspberry Pi 宣布与 Hailo 公司合作开发,推出了一款售价 70 美元的扩展套件 AI Kit:它提供了一种便捷的方法,可将本地高性能、高能效推理集成到各种应用中,适用于 Raspberry Pi 5。 根据介绍,Raspberry Pi AI Kit 预装了最新发布的 Raspberry Pi M.2 HAT+ 和具有 13 TOPS 推理性能的 Hailo-8L M.2 AI 加速模块。 “安装在 Raspberry Pi 5 上的 AI Kit 可让你快速构建复杂的 AI 视觉应用程序,并以低延迟和低功耗要求实时运行。用于对象检测、语义和实例分割、姿势估计和面部标志(仅举几例)的最先进的神经网络完全在 Hailo-8L 协处理器上运行,让 Raspberry Pi 5 CPU 可以自由执行其他任务。” Raspberry Pi AI Kit 的主要功能包括: 每秒 13 万亿次运算 (TOPS) 的推理性能 以 8Gbps 运行的单通道 PCIe 3.0 连接 与 Raspberry Pi 图像软件子系统完全集成 与第一方或第三方相机的兼容性 加速器硬件的高效调度...
- 下一篇
Zadig 通过信创认证!KodeRover 正式成为信创工委会成员
近日,KodeRover 自主研发的云原生 DevOps 平台 Zadig,依据《信息技术应用创新产品评估规范》(T/SSIA 2001-2022),荣获"信创产品评估证书",并正式成为信创工委会成员单位。 信创产业的重要意义 信创,即信息技术应用创新产业,信创产业的核心是建立自主可控的信息技术底层架构和标准,实现全产业链的自主可控。对国家经济的稳定发展和国际竞争力具有重大意义。 推动产研数字化转型 通过发展信创产业推动数字化转型,不仅关乎经济内生动力的培育,更关系到未来我国经济的稳定发展和国际竞争力的持续增强。作为国家”新基建“发展战略,国产信创生态的建设对于加快企业数字化转型、为企业赋能增效、推动经济持续发展具有重要意义。 Zadig 是 KodeRover 公司推出的面向开发者的自助式云原生 DevOps 平台,旨在协助产研团队实现云原生持续交付,帮助企业以更先进、更经济、更安全、更轻便的方式实现数字化研发转型。通过 Zadig 平台,助力企业实现数字化转型,赋能增效,加速平台工程等新一代核心技术国产化替代。 Zadig 的广泛应用与认可 自开源以来,Zadig 以其卓越的云原生...
相关文章
文章评论
共有0条评论来说两句吧...
文章二维码
点击排行
推荐阅读
最新文章
- SpringBoot2整合Thymeleaf,官方推荐html解决方案
- MySQL8.0.19开启GTID主从同步CentOS8
- SpringBoot2更换Tomcat为Jetty,小型站点的福音
- Red5直播服务器,属于Java语言的直播服务器
- CentOS7,8上快速安装Gitea,搭建Git服务器
- CentOS6,7,8上安装Nginx,支持https2.0的开启
- CentOS8,CentOS7,CentOS6编译安装Redis5.0.7
- Jdk安装(Linux,MacOS,Windows),包含三大操作系统的最全安装
- SpringBoot2整合MyBatis,连接MySql数据库做增删改查操作
- SpringBoot2全家桶,快速入门学习开发网站教程