【周文20180615】JavaScript 引擎基本原理:对象状态和内部缓存

Table of Contents

  1. 来源
  2. JavaScript 引擎管道(Engine Pipeline)
  3. JavaScript 引擎中的解释器和编译器管道(interpreter/compiler pipelines)
  4. JavaScript 对象模型(Object Model)
  5. 优化属性的访问(Optimizing property access)
    1. 对象属性的访问方式:
    2. 同结构对象存储优化
    3. 空对象的属性增加
    4. 结构树,多对象不同属性的赋值
    5. 一切以原始结构为基础
  6. 访问优化背后的原理:内部缓存(Inline Caches, ICs)
  7. 数组对象的优化
  8. 警示
    1. 总是以相同的结构去初始化对象,确保引擎对这些对象采取同一个结构树去优化。
    2. 切记不要对数组的属性(下标)设置属性描述信息,防止引擎对整个数组以 map 结构去处理,而导致性能问题。

来源

本文及文中使用的图片均来自 https://mathiasbynens.be/notes/shapes-ics 阅读翻译而来,仅供个人学习记录使用。

JavaScript 引擎管道(Engine Pipeline)

引擎对代码的解析基本过程:

  1. 将源码解析成 AST(Abstract Syntax Tree, 抽象语法树, AST解析插件: esprima);
  2. 通过解释器(interpreter),基于解析后的 AST 将源码处理成字节码(bytecode);
  3. 在正常解释成字节码之后,引擎便可以真正开始执行代码了

流程图:

img

为了让引擎处理速度加快,在处理字节码(bytecode)的时候,解释器会携带代码性能分析
数据一起来分析。解释器(interpreter)会在性能分析数据(profiling data)的基础上
做一些假设,然后生成高度优化的机器码(machine code)。

大概意思就是,引擎会将解释器结合性能分析数据去将字节码解析成高度优化的机器码。

如果在解析过程中发生错误,或解析失败,优化器(optimizing compiler)会将已经解析
的代码还原回去,并且重新回到解释器那一步。

JavaScript 引擎中的解释器和编译器管道(interpreter/compiler pipelines)

通常来讲,在一个完整的解析代码至正确运行代码之前的过程中都会有一个解释器
(interpreter)去将代码解释成未优化的字节码(bytecode),并且会有一个优化器
(optimizing compiler)将解释后的字节码生成高度优化的机器码,这个过程中解释相对
来说是很快的,在优化的时候可能需要花费更长的时间。

  • v8 中的区别只是命名不太相同,具体的处理逻辑是相似的:

    img

    只是解释器不是使用 interpreter 而是 Ignition ,而 optimize compiler
    优化器则是 TurboFan

  • Mozila 浏览器引擎处理

    img

    Mozila 浏览器引擎中的处理会稍微有点不同,它具有两个优化器(optimizing
    compiler
    ),分别是: Baseline 优化器和 IonMonkey 优化器。在解释器解释成字节
    码之后会首先经过 Baseline 优化器对字节码进行重度优化(heavily-optimized),然后携带性能分析数据交给 IonMonkey 优化器进一步优化,如果优化过程中出现错误会回退优化内容回到 Baseline 优化后的代码状态(somewhat optimized code)。

  • 微软的 Chakra 引擎

    Chakra 中的处理和 Mozila 类似,具有两个优化器

    1. SimpleJIT 优化部分字节码,然后协同分析数据给下一个优化器
    2. FullJIT 优化经过 SimpleJIT 重度优化之后的代码。

    img

  • 苹果引擎 JavaScriptCore(JSC)

    JSC 会有一个解释器(LLInt, Low-Level Interpreter),三个优化器,

    1. Baseline 负责优化部分代码
    2. DFG 到此会有大部分代码被优化,错误回到 Baseline 优化后的状态
    3. FTL 优化剩余的代码,错误回到 Baseline 优化后的状态

    img

    苹果选择使用三个优化器,有其利弊权衡,这个权衡点就在快速获得可运行的代码和花费更多的时间去获取优化后的高性能的可运行的代码,简而言之,在某些时候需要用时间换
    取高性能。并且增加多个优化器在某种意义上来说就获得了更多对代码在运行之前的控制权,在这期间可以利用这些控制权去对代码进行更多的处理及控制。

    1. 解释器虽然能可很快的将代码转换成字节码,但是通常并不是很高效
    2. 一个优化器在另一方面来说可能需要更长时间,却可以产生更加高效的机器码
  • 引擎总结

    在总体上来说,所有的引擎处理基本架构都是相似的,主要的差别在于优化器的个数和处理的细微不同

    1. V8 引擎和通用版只是命名的差别,解释器命名 interpreter -> Ignition
      优化器命名 optimizing compiler -> TurboFan

    2. Mozilla 浏览器引擎,有两个优化器: BaselineIonMonkey

    3. Chakra 微软引擎,也有两个优化器: SimpleJITFullJIT
    4. JavaScriptCore, JSC 苹果引擎,有三个优化器: Baseline, DFG, FTL
      并且每个优化器只负责部分代码的优化工作。

JavaScript 对象模型(Object Model)

通过分析 JavaScript 中的对象模型,我们来熟悉下引擎是如何去实现它,又是如何去加
速对象属性的访问速度的。

ECMAScript 标准中使用字符串作为对象属性的 key 以字典的方式去描述对象,即以字
符串作为字典的 key ,以属性描述符对象方式来描述该 key 对应的值。

img

上述引擎会将对象的每个属性值,使用属性描述符对象来存储,如果要取得这个对象,可通过 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 值为对应的数组元
素。值的定义方式也是采用属性描述符的形式定义。

img

每次只要有数组的一个元素值发生变化,会相应的改变描述符对象中的 [[Value]] 值。

img

优化属性的访问(Optimizing property access)

对象属性的访问方式:

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 在实际对象中的偏移位置(索引,即对象也是分先后顺序的)。

如下图:

img

在这种结构中,针对多个同构对象,便省去了为每个对象分配一份 key 以及属性对象。

img

从分析及图中可知,这种存储结构也是有一定局限性的,个人理解,如果需要为每个对象设置不同的属性描述符值时就会不太适用了,不知引擎是不是会在这种结构基础上去区分属性描述符对象的内容,如果不同估计会采取不同的结构去存储。


虽然各个浏览器引擎都使用了这种优化方式,但是叫法各不相同:

  • Academic papers 叫: Hidden Classes
  • V8 叫: Maps
  • Chakra 叫: Types
  • JavaScriptCore, JSCStructures
  • SpiderMonkeyShapes

空对象的属性增加

对空对象的处理,首先会创建一个空的结构对象,里面什么都没有,如果给空对象增加了新的属性,会在空结构中不断追加,最后的那个结构会包含完整的同构对象中的所有属性。

img

整个结构在不断追加属性的过程中会不断增长,而形成一个链条,此时处理会有两种方式:

一是越往后的链条中的节点会同时包含前一个链条中的内容,另一个是往后的节点不保存前一个链条的内容,只维系链条之间的关系,由此可以通过这种关系去找到上一个节点的内容。

如下图:

img

如果需要修改链条中某个属性的值,引擎会循着该链条路径去查找,直至找到目标属性然后去设置其对应的值。

结构树,多对象不同属性的赋值

即两个不同的空对象,分别对其赋予不同的新属性,引擎会在一个空结构之上创建两个结构来应对这种情况,同理如果是多个对象不同结构则会产生多个结构对象,这种成为结构树。

const obj1 = {}
obj1.a = 5
const obj2 = {}
obj2.b = 6

结构图:

img

一切以原始结构为基础

引擎在优化的时候以原始结构为基础来创建结构对象,即原始是什么结构该对象的优化会以相对应的原始结构对象来进行。

比如:

const obj1 = {}
obj1.a = 5
const obj2 = { a: 5 }

上面的代码从结果上来看, obj1obj2 的结构是一致的,都是 { a: 5 } ,但他
们的原始结构却不相同(即声明时候的结构), obj1{} 孔结构, obj2{ a:
5 }
,因此引擎会针对这种情况分别创建两个结构对象,如下图:

img

假如:

const obj1 = {}
obj1.a = 5
const obj2 = {}
obj2.b = 6
const obj3 = { a: 5 }
const obj4 = { a: 6 }

如果是这种情况,图中左边的空解构下面会有两个节点,一个是 shape(a) ,另一个是
shape(b) ,右边的依旧是一个,但是对应的值会是两个 56

因为 obj1obj2 的原始结构都是 shape(empty)obj3obj4 都是shape(a)

访问优化背后的原理:内部缓存(Inline Caches, ICs)

内部缓存是对象属性访问优化背后重要的原理,也是让 JavaScript 能加快运行的关键因
素,引擎会利用 ICs 去记住去何处查找对象属性的重要信息,从而减少昂贵的查找次数。

获取对象属性的函数实例(在苹果引擎 JSC 中运行):

function getX(obj) {
  return obj.x
}

生成对应的字节码:

img

第一句 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 })

img

查找过程:首先找到 {x: 5} 对象中 x 属性键所在的 shape(x) ,然后根据结构中
存储的 offset 偏移量在 get_by_id 指令的存储空间中找到本次取值的结果。

img

很明显,其实在引擎采取这种优化方式存储对象的时候,在整个存储结构中肯定有一个链条能将所有关联的信息链接起来形成一个闭环。

如: 对象解析 {x:5} -> 结构存储(shape(x)) -> 设置结构存储偏移量 ->
get_by_id 根据偏移量存储查找 -> 找到结构中的对应属性(shape(x) 中的 x) ->
得到值 5 返回结果

因此,整个过程中查找的关键就在结构存储中的 offset 偏移量。

由此,可以避免频繁的对对象的属性信息进行昂贵的查找操作,而只需要查看对象在 IC
中记录的结构信息,然后根据结构中的偏移量去查找。

数组对象的优化

根据对象的存储优化原理: ShapeIC ,同样也适用于数组

来看看数组是怎么做的

实例:

const array = [
  '#jsconfeu'
]

在数组对象中,存在一个 length 属性,该属性会和我们之前看到的对象属性方式类似

img

但是针对数组元素的的处理不太相同,即每个数组都会有一个独立的数组元素备份仓库,而长度采用 shapeoffset 方式处理

img

引擎并不需要去存储每个数组元素对应的属性,毕竟数组元素通常都是可写(writable),
可枚举(enumerable),和可配置的(configurable)。

但是切记不要为数组元素的属性(即下标)单独设置属性描述信息,因为一旦这么操作之后引擎会将将整个数组的元素备份当做字典来处理,这将会导致引擎的处理速度大大降低。

比如这样:

// Please don’t ever do this!
const array = Object.defineProperty(
  [],
  '0',
  {
    value: 'Oh noes!!1',
    writable: false,
    enumerable: false,
    configurable: false,
  }
);

最后的结果是:

img

因此

切记不要给数组属性(下标)设置属性描述信息。

警示

总是以相同的结构去初始化对象,确保引擎对这些对象采取同一个结构树去优化。

切记不要对数组的属性(下标)设置属性描述信息,防止引擎对整个数组以 map 结构去处理,而导致性能问题。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值