一些js引擎
- 浏览器环境: Chakra, JavaScriptCore, Spidermonkey, V8
- node环境: Chakra, V8, SpiderNode
- IoT: Duktape, JerryScript
总结:·现代js引擎在构造上都包含1个interpreter + n个JIT(n>=1)
v8引擎编译器发展演变
- 5.9版本之前 v8引擎使用Cranshaft编译器直接将AST(抽象语法树)编译成机器码
- 5.9版本之后,v8引擎使用字节码解释器 + JIT 编译器
出于对内存的考虑,v8引擎不再直接将全部代码编译成机器码
v8引擎编译过程
优化(optimize)和去优化(Deoptimize)
- 当Ignition生成字节码的过程中,TurboFan会分析这个过程, 并且会对"hot" function进行优化编译。这些"hot" function会被直接编译生成执行效率更高的机器码。下次再次遇到这些“hot function”时,就不需要再编译成字节码,而是直接使用已有的机器码。
优化的威力
const v8 = require('v8-natives');
const { performance, PerformanceObserver } = require('perf_hooks')
function test(x, y) {
return x + y
}
const obs = new PerformanceObserver((list, observer) => {
console.log(list.getEntries())
observer.disconnect()
})
obs.observe({ entryTypes: ['measure'], buffered: true })
performance.mark('start')
let number = 10000000 //20000000
// 不优化代码
// v8.neverOptimizeFunction(test)
while (number--) {
test(1, 2)
}
performance.mark('end')
performance.measure('test', 'start', 'end')
复制代码
执行1千万次⬆︎
执行2千万次⬆︎
- ⚠️注意:并不是每次调用"hot" function时都可以直接使用优化后的机器码,TurboFan会去比对此次调用方法中的类型信息,当获取的类型信息不符合预期时,编译将会降级回退到Ignition。这个回退过程称之为:Deoptimize。优化回滚的过程通常都会导致性能的损失。
function test(x, y) {
return x + y
}
let number = 100000;
while (number--) {
test(1, 2)
}
test('a', 'b') //类型信息不符合预期,优化回滚
复制代码
属性访问优化
隐藏类(shape)
JavaScript每个对象都是自描述,属性信息都包含在自身的结构中。
let a = {x: 5, y: 6}
let b = {x: 7, y: 8}
复制代码
对于相同结构的对象而言,将每个属性的信息都存储一遍是很大的内存开销,因而在js引擎内部使用隐藏类这种结构来存储属性信息
Transition chains
js是动态语言,我们可以随时为对象增添属性,当给对象新增key时,此时会进行一个shape的链式创建的过程。
在js对象的内部有一个指向当前shape类型的指针,为了优化属性的访问速度,js引擎还使用shapeTable来记录对象属性在shape链中所在的位置。
a = {}
a.x = 6
b = {x: 6}
复制代码
js引擎为了减少shape链的长度,会对已经具有的属性的对象进行优化,直接生成包含有属性的shape类型,而非从空的shape类型开始创建。
在创建对象时,尽可能将对象的所有属性一次性申明,这样可以减少shape链的长度,优化属性的访问速度。
内联缓存(Inline Caches)
内联缓存(Inline caching)是部分编程语言的运行时系统采用的优化技术,最早为Smalltalk开发。内联缓存的目标是通过记住以前直接在调用点上方法查询的结果来加快运行时方法绑定的速度。内联缓存对动态类型语言尤为有用,其中大多数(如非全部)方法绑定发生在运行时。 内联缓存的概念基于观察到的经验,即发生在特定调用点(call site)的对象通常是相同的类型。在这种情况下,存储方法查询内联结果可以大幅提升性能,即直接抵达调用点。
js引擎使用ICs来缓存对象属性的位置信息,从而减少向上查找的次数。
js引擎生成两条指令:第一条指令是从第一个入参对象中找到x属性,并将其值存储在loc0中;第二个指令是返回loc0
当第一次调用getX方法时,ICs会识别出入参的shape类型,找到x属性的位置信息,并且还会将入参的shape类型和x属性的位置信息缓存起来。这样在后面的方法调用中,js引擎只需先比对传入的shape类型是否相同,如果相同,则就可以从缓存的位置信息中直接获取属性的值。
Tips
- 不要经常改变对象类型❌
- 使用类似Typescript带有静态类型检查的语言✅