interpreter/compiler pipelines
)map
结构去处理,而导致性能问题。本文及文中使用的图片均来自 https://mathiasbynens.be/notes/shapes-ics 阅读翻译而来,仅供个人学习记录使用。
引擎对代码的解析基本过程:
interpreter
),基于解析后的 AST
将源码处理成字节码(bytecode
);流程图:
为了让引擎处理速度加快,在处理字节码(bytecode
)的时候,解释器会携带代码性能分析
数据一起来分析。解释器(interpreter
)会在性能分析数据(profiling data
)的基础上
做一些假设,然后生成高度优化的机器码(machine code
)。
大概意思就是,引擎会将解释器结合性能分析数据去将字节码解析成高度优化的机器码。
如果在解析过程中发生错误,或解析失败,优化器(optimizing compiler
)会将已经解析
的代码还原回去,并且重新回到解释器那一步。
interpreter/compiler pipelines
)通常来讲,在一个完整的解析代码至正确运行代码之前的过程中都会有一个解释器
(interpreter
)去将代码解释成未优化的字节码(bytecode
),并且会有一个优化器
(optimizing compiler
)将解释后的字节码生成高度优化的机器码,这个过程中解释相对
来说是很快的,在优化的时候可能需要花费更长的时间。
在 v8
中的区别只是命名不太相同,具体的处理逻辑是相似的:
只是解释器不是使用 interpreter
而是 Ignition
,而 optimize compiler
优化器则是 TurboFan
。
Mozila
浏览器引擎处理
在 Mozila
浏览器引擎中的处理会稍微有点不同,它具有两个优化器(optimizing
),分别是:
compilerBaseline
优化器和 IonMonkey
优化器。在解释器解释成字节
码之后会首先经过 Baseline
优化器对字节码进行重度优化(heavily-optimized
),然后携带性能分析数据交给 IonMonkey
优化器进一步优化,如果优化过程中出现错误会回退优化内容回到 Baseline
优化后的代码状态(somewhat optimized code
)。
微软的 Chakra
引擎
Chakra
中的处理和 Mozila
类似,具有两个优化器
SimpleJIT
优化部分字节码,然后协同分析数据给下一个优化器FullJIT
优化经过 SimpleJIT
重度优化之后的代码。苹果引擎 JavaScriptCore(JSC)
JSC
会有一个解释器(LLInt, Low-Level Interpreter
),三个优化器,
Baseline
负责优化部分代码DFG
到此会有大部分代码被优化,错误回到 Baseline
优化后的状态FTL
优化剩余的代码,错误回到 Baseline
优化后的状态苹果选择使用三个优化器,有其利弊权衡,这个权衡点就在快速获得可运行的代码和花费更多的时间去获取优化后的高性能的可运行的代码,简而言之,在某些时候需要用时间换
取高性能。并且增加多个优化器在某种意义上来说就获得了更多对代码在运行之前的控制权,在这期间可以利用这些控制权去对代码进行更多的处理及控制。
引擎总结
在总体上来说,所有的引擎处理基本架构都是相似的,主要的差别在于优化器的个数和处理的细微不同
V8
引擎和通用版只是命名的差别,解释器命名 interpreter
-> Ignition
,
优化器命名 optimizing compiler
-> TurboFan
Mozilla
浏览器引擎,有两个优化器: Baseline
和 IonMonkey
Chakra
微软引擎,也有两个优化器: SimpleJIT
和 FullJIT
JavaScriptCore, JSC
苹果引擎,有三个优化器: Baseline
, DFG
, FTL
, 通过分析 JavaScript
中的对象模型,我们来熟悉下引擎是如何去实现它,又是如何去加
速对象属性的访问速度的。
ECMAScript
标准中使用字符串作为对象属性的 key
以字典的方式去描述对象,即以字
符串作为字典的 key
,以属性描述符对象方式来描述该 key
对应的值。
上述引擎会将对象的每个属性值,使用属性描述符对象来存储,如果要取得这个对象,可通过 Object.getOwnPropertyDescriptor(obj, 'foo')
来获取对象中某个属性对应的值
的描述符对象,如:
const person = { name: 'lzc' }
const desc = Object.getOwnPropertyDescriptor(person, 'name')
console.log(desc, 'the description of `name` in `person`')
+RESULTS:
{ value: 'lzc',
writable: true,
enumerable: true,
configurable: true } 'the description of `name` in `person`'
对于数组而言, JavaScript
对其处理和对象类似,将 length
作为长度的 key
,
值为数组长度,数组的索引下标作为数组对象的每个元素对应的 key
值为对应的数组元
素。值的定义方式也是采用属性描述符的形式定义。
每次只要有数组的一个元素值发生变化,会相应的改变描述符对象中的 [[Value]]
值。
const obj = {
foo: 'bar',
baz: 'qux'
}
// visit
// doSth(obj.foo)
// 通常情况下会出现不同对象中的结构几乎是一样的情况
const objA = { x: 1, y: 2 }
const objB = { x: 3, y: 4 }
// 这样我们可以声明一个专门用来取其中一个属性的函数,只能在命名上区别,例如
const logX = obj => console.log(obj.x)
// 输出
logX(objA)
logX(objB)
+RESULTS:
1
3
通过声明 logX
函数,用来专门从某一个对象中取出 x
属性的值。
在正常对象访问当中,调用 obj.y
时候,引擎首先会查找对象中的 key: y
,然后根
据这个 key
去查找对应的包含属性描述符的对象,最后将该对象中的 [[Value]]
对应
的值返回。
但是在操作相同结构的对象时候,如果采用普通方式存储,那么每个对象将分配一个空间,保存这其中所有的 key
和其对应的属性对象,由于这些对象的结果是相同的,因此可以从其 key
的保存方式上进行统一管理,在值和属性对象中间加一层“对象形状”对象,改
对象负责保存对象中所有的 key
,这些 key
又对应着每个 key
的描述符对象,原来的 [[Value]]
只需要替换成该 key
在实际对象中的偏移位置(索引,即对象也是分先后顺序的)。
如下图:
在这种结构中,针对多个同构对象,便省去了为每个对象分配一份 key
以及属性对象。
从分析及图中可知,这种存储结构也是有一定局限性的,个人理解,如果需要为每个对象设置不同的属性描述符值时就会不太适用了,不知引擎是不是会在这种结构基础上去区分属性描述符对象的内容,如果不同估计会采取不同的结构去存储。
虽然各个浏览器引擎都使用了这种优化方式,但是叫法各不相同:
Academic papers
叫: Hidden ClassesV8
叫: MapsChakra
叫: TypesJavaScriptCore, JSC
叫 StructuresSpiderMonkey
叫 Shapes对空对象的处理,首先会创建一个空的结构对象,里面什么都没有,如果给空对象增加了新的属性,会在空结构中不断追加,最后的那个结构会包含完整的同构对象中的所有属性。
整个结构在不断追加属性的过程中会不断增长,而形成一个链条,此时处理会有两种方式:
一是越往后的链条中的节点会同时包含前一个链条中的内容,另一个是往后的节点不保存前一个链条的内容,只维系链条之间的关系,由此可以通过这种关系去找到上一个节点的内容。
如下图:
如果需要修改链条中某个属性的值,引擎会循着该链条路径去查找,直至找到目标属性然后去设置其对应的值。
即两个不同的空对象,分别对其赋予不同的新属性,引擎会在一个空结构之上创建两个结构来应对这种情况,同理如果是多个对象不同结构则会产生多个结构对象,这种成为结构树。
const obj1 = {}
obj1.a = 5
const obj2 = {}
obj2.b = 6
结构图:
引擎在优化的时候以原始结构为基础来创建结构对象,即原始是什么结构该对象的优化会以相对应的原始结构对象来进行。
比如:
const obj1 = {}
obj1.a = 5
const obj2 = { a: 5 }
上面的代码从结果上来看, obj1
和 obj2
的结构是一致的,都是 { a: 5 }
,但他
们的原始结构却不相同(即声明时候的结构), obj1
是 {}
孔结构, obj2
是 { a:
,因此引擎会针对这种情况分别创建两个结构对象,如下图:
5 }
假如:
const obj1 = {}
obj1.a = 5
const obj2 = {}
obj2.b = 6
const obj3 = { a: 5 }
const obj4 = { a: 6 }
如果是这种情况,图中左边的空解构下面会有两个节点,一个是 shape(a)
,另一个是
shape(b)
,右边的依旧是一个,但是对应的值会是两个 5
和 6
。
因为 obj1
和 obj2
的原始结构都是 shape(empty)
, obj3
和 obj4
都是shape(a)
。
内部缓存是对象属性访问优化背后重要的原理,也是让 JavaScript
能加快运行的关键因
素,引擎会利用 ICs
去记住去何处查找对象属性的重要信息,从而减少昂贵的查找次数。
获取对象属性的函数实例(在苹果引擎 JSC
中运行):
function getX(obj) {
return obj.x
}
生成对应的字节码:
第一句 get_by_id loc0, arg1, x
的意思是从参数 arg1
对象中查找 x
属性对应的
值,然后将该值保存到 loc0
第二句 return loc0
返回查找并保存的值
另外从图中可以看出,在 get_by_id
指令中,后面还有两个 N/A
标识的空间,这种就
是用来根据偏移量来存储同结构不同属性值的值。
实例:
function getX(obj) {
return obj.x
}
getX({ x: 5 })
查找过程:首先找到 {x: 5}
对象中 x
属性键所在的 shape(x)
,然后根据结构中
存储的 offset
偏移量在 get_by_id
指令的存储空间中找到本次取值的结果。
很明显,其实在引擎采取这种优化方式存储对象的时候,在整个存储结构中肯定有一个链条能将所有关联的信息链接起来形成一个闭环。
如: 对象解析 {x:5}
-> 结构存储(shape(x)
) -> 设置结构存储偏移量 ->
get_by_id
根据偏移量存储查找 -> 找到结构中的对应属性(shape(x)
中的 x
) ->
得到值 5
返回结果
因此,整个过程中查找的关键就在结构存储中的 offset
偏移量。
由此,可以避免频繁的对对象的属性信息进行昂贵的查找操作,而只需要查看对象在 IC
中记录的结构信息,然后根据结构中的偏移量去查找。
根据对象的存储优化原理: Shape
和 IC
,同样也适用于数组
来看看数组是怎么做的
实例:
const array = [
'#jsconfeu'
]
在数组对象中,存在一个 length
属性,该属性会和我们之前看到的对象属性方式类似
但是针对数组元素的的处理不太相同,即每个数组都会有一个独立的数组元素备份仓库,而长度采用 shape
加 offset
方式处理
引擎并不需要去存储每个数组元素对应的属性,毕竟数组元素通常都是可写(writable
),
可枚举(enumerable
),和可配置的(configurable
)。
但是切记不要为数组元素的属性(即下标)单独设置属性描述信息,因为一旦这么操作之后引擎会将将整个数组的元素备份当做字典来处理,这将会导致引擎的处理速度大大降低。
比如这样:
// Please don’t ever do this!
const array = Object.defineProperty(
[],
'0',
{
value: 'Oh noes!!1',
writable: false,
enumerable: false,
configurable: false,
}
);
最后的结果是:
因此
切记不要给数组属性(下标)设置属性描述信息。
map
结构去处理,而导致性能问题。