阿八博客
  • 100000+

    文章

  • 23

    评论

  • 20

    友链

  • 最近新加了很多技术文章,大家多来逛逛吧~~~~
  • 喜欢这个网站的朋友可以加一下QQ群,我们一起交流技术。

【译】JavaScript engine fundamentals: optimizing prototypes

欢迎来到阿八个人博客网站。本 阿八个人博客 网站提供最新的站长新闻,各种互联网资讯。 喜欢本站的朋友可以收藏本站,或者加QQ:我们大家一起来交流技术! URL链接:https://www.abboke.com/jsh/2019/1010/116433.html

前言

前往 ➡️ 我的博客

本文是根据自己的理解翻译而来,如有疑惑可查看原文 JavaScript engine fundamentals: optimizing prototypes

本次暂定翻译三篇文章:

JavaScript engine fundamentals: Shapes and Inline Caches(Published 14th June 2018)JavaScript engine fundamentals: optimizing prototypes(Published 16th August 2018)The story of a V8 performance cliff in React(Published 28 August 2019)

Optimization tiers and execution trade-offs

上一篇文章已经讨论了现代 JavaScript 引擎的工作流程:

解释器可以很快地生成字节码,但是字节码的效率不高
另一方面,优化编译器虽然会稍微花费些时间,却可以生成效率更高的机器码

下图是 V8 模型,V8 的解释器称为 Ignition,是所有引擎中最快的解释器(从原始字节码执行速度的角度)
V8 的优化编译器称为 TurboFan,它最终会生成高度优化的机器码

解释器可以快速生成字节码,但是字节码执行的速度比较慢
Baseline 会花些时间生成代码,但同样会提供性能更好的代码
最后,IonMonkey 会花更长的时间去生成机器码,并能够更高效地执行

来用一个具体的例子,看看不同引擎之间的处理差异
在这个循环里,一些代码重复执行

当优化程序进行时,V8 继续执行字节码
在某个时刻,优化程序生成可执行代码后,流程会接着执行下去

同样,SpiderMonkey 也是在解释器中开始执行字节码
但是它有 Baseline 层,hot 代码会被送到这里
一旦 Baseline 编译器生成了 Baseline 代码,流程会接着执行下去

当代码准备妥当后,引擎开始执行 SimpleJIT 代码
这种方式的好处在于复制所停留的时间远远小于编译器( 编译器 frontend)所用的时间
缺点就是,这种启发式复制(copy heuristic)会使得某种优化所必须的信息丢失,因此这是在用代码质量换取时间

在 JavaScriptCore,所有的优化编译器和主线程并发运行;主线程只是触发了另一个线程的编译任务
然后编译器通过复杂的加锁从主线程获取分析数据(profiling data)

以下是 V8 的 Ignition 编译器生成的字节码:

和字节码相比较,这里的代码会显得很多!通常来说,字节码会比机器码紧凑得多,尤其对比高度优化过的机器码
另一方面,字节码需要解释器来运行,而优化过的代码则可以被处理器直接执行

这是 JavaScript 引擎不「优化一切」的原因之一(仅优化 「hot function」)
正如我们早先看到的,生成优化过的机器码会用很长的时间,除此之外,我们刚才也知道了优化过的机器码会占用用更多的内存空间

结合 Shapes 和 Inline Caches 可以加快代码中同一位置的属性重复性访问

看似是个新概念,其实就是基于原型的语法糖

创建出来的实例(foo)拥有一个只包含属性 'x' 的 shape
foo 的原型指向 Bar.prototype

如果你用同一个类又创建了一个实例,那么这两个实例将共享 shape,两个实例也会指向同一个 Bar.prototype

Prototype property access

ok,我们已经知道了定义一个类并用类创建实例的过程
那么,如果我们在实例上调用一个方法,又会发生什么呢?

步骤 1:加载方法,这个方法只不过是原型上的属性(而它恰好是个函数)
步骤 2:用实例去调用这个方法(重新绑定 this
先看步骤 1:

这个例子中,foo.getX() 被调用了两次,但是每次都会有不同的含义,不同的结果
所以说,尽管原型在 JavaScript 中只是个对象,但是提升原型属性的访问速度依然比常规对象更具有挑战性

通常情况下,原型属性的加载是个非常频繁的操作:每次方法调用都会去加载属性!

在这个案例中,为了提高重复加载的速度,我们需要知道三件事:

foo 的 shape 不包含 getX 且没有改变过
这意味着 foo 没有添加、删除属性,或改变属性特性
foo 的原型依然是 Bar.prototype
这意味着,foo 的原型没有通过 Object.setPrototypeOf()__proto__ 的方式改变过
Bar.prototype 的 shape 包含 getX 且没有改变过
这意味着 Bar.prototype 没有添加、删除属性,或改变属性特性

一般情况下,这意味着我们需要检查 1 遍实例本身,还有因每增加一个原型就就要增加的 2 遍检查直到找到我们想要的属性
1+2N(N 表示原型链上直到找到存在属性的原型的原型数量) 遍的检查看上去还不是特别糟糕,因为这时的原型链还比较短 —— 但是引擎会经常处理有着很长原型链的对象,就比如常见的 DOM 类

getAttribute() 是在 Element.prototype 上发现的
这意味着我们每次调用 anchor.getAttribute() 时,都需要做以下这些事:

检测到 getAttribute 不存在于 anchor 对象本身;检测到 anchor 的原型是 HTMLAnchorElement.prototype;确认没有 getAttribute 属性;检测到下一个原型是 HTMLElement.prototype;确认没有 getAttribute 属性;继续检测下一个原型 Element.prototype;找到 getAttribute

一共需要 7 次检测!而这种情况很常见,于是引擎想方设法去减少属性(原型上)加载时的检查次数

回到更早的例子,当我们从 foo 访问 getX 时,共做了 3 次检查:

每个 shape 都指向了原型,这意味着 foo 的原型改变时,引擎会自动过渡到新的 shape
现在我们只需要检查对象的 shape 就可以同时检测属性是否存在以及原型链的导向

鉴于此,由于检查的次数从 1+2N 降低到 1+N,所以原型上属性的访问速度也变快了
由于在原型链上查找属性的时间复杂度是线性的,所以依然还是很耗时的
引擎使用了不同的方法让检查的次数趋于常量,尤其是同一属性的连续加载(访问)

Validity cells

为此,V8 特别处理了原型的 shapes
每个原型都有一个独一无二的 shape,这个 shape 不会被其它的对象共享(特别是其它的原型对象),每一个原型的 shape 都有与之关联的 ValidityCell

代码第一次执行时,ICs 开始工作了,它要缓存属性在原型上的偏移量 「Offset」,属性所在的原型 「Prototype」(本例中的 Bar.prototype),实例的 shape 「Shape