在不同的对象中使用相同的键时,在V8中调用缓慢的函数

也许不是因为这个电话很慢,而是这个查询是; 我不确定,但这是一个例子:

var foo = {}; foo.fn = function() {}; var bar = {}; bar.fn = function() {}; console.time('t'); for (var i = 0; i < 100000000; i++) { foo.fn(); } console.timeEnd('t'); 

testing在win8.1上

  • 火狐35.01:〜240ms
  • 铬40.0.2214.93(V8 3.30.33.15): 〜760ms
  • msie 11:34
  • nodejs 0.10.21(V8 3.14.5.9):〜100ms
  • iojs 1.0.4(V8 4.1.0.12): 〜760ms

现在这里是有趣的部分,如果我改变bar.fn bar.somethingelse

  • 铬40.0.2214.93(V8 3.30.33.15):〜100ms
  • nodejs 0.10.21(V8 3.14.5.9):〜100ms
  • iojs 1.0.4(V8 4.1.0.12):〜100ms

v8最近出了什么问题? 这是什么原因?

第一个基本面

V8使用与转换相关的 隐藏类来发现蓬松的无形JavaScript对象中的静态结构。

隐藏的类描述对象的结构,将隐藏的类链接到一起,如果对某个对象执行特定的操作,则会将隐藏的类连接起来,从而描述应该使用哪个隐藏的类。

例如下面的代码将导致下面的隐藏类链:

 var o1 = {}; o1.x = 0; o1.y = 1; var o2 = {}; o2.x = 0; o2.y = 0; 

在这里输入图像说明

这个链是在你构造o1创build的。 当构buildo2 ,V8简单地遵循已build立的转换。

现在,当一个属性fn被用来存储一个函数时,V8会试图给这个属性一个特殊的处理:而不是仅仅在隐藏的类中声明该对象包含一个属性。

 var o = {}; o.fn = function fff() { }; 

在这里输入图像说明

现在这里有一个有趣的结果:如果将不同的函数存储到具有相同名称的字段中,V8不能再简单地跟随转换,因为函数属性的值不符合期望值:

 var o1 = {}; o1.fn = function fff() { }; var o2 = {}; o2.fn = function ggg() { }; 

当评估o2.fn = ...赋值时,V8将会看到有一个标记为fn的转换,但是会导致一个不适合的隐藏类:它在fn属性中包含fff ,而我们正试图存储ggg 。 注意:为了简单起见,我给出了函数名称–V8不在内部使用它们的名称,而是使用它们的标识

由于V8无法遵循这一转变,V8将决定其向隐藏类提升function的决定是不正确和浪费的。 照片会改变

在这里输入图像说明

V8将创build一个新的隐藏类,其中fn只是一个简单的属性,而不是一个常量函数属性了。 它将重新转换转换,并标记旧的转换目标。 请记住, o1仍在使用它。 然而,下一次代码会触及o1例如,当一个属性被加载时,运行时会将o1从已弃用的隐藏类迁移出来。 这样做是为了减less多态性 – 我们不希望o1o2有不同的隐藏类。

为什么在隐藏类上有function很重要? 因为这给了V8优化的编译器信息,它用来内联方法调用 。 如果调用目标存储在隐藏类本身上,则只能内联方法调用。

现在让我们把这个知识应用到上面的例子中。

因为转换之间存在冲突, bar.fnfoo.fn成为常规属性 – 函数直接存储在这些对象上,而V8不能内联foo.fn的调用, foo.fn导致性能降低。

它可以在之前内线吗? 是的 。 这是什么改变了:在旧的V8 没有折旧机制,所以即使我们有一个冲突和重新路由过渡, foo并没有迁移到隐藏的类,其中fn成为一个正常的财产。 相反, foo仍然保留隐藏类,其中fn是一个常量函数属性,直接embedded到隐藏类中,允许优化编译器将其内联。

如果您尝试在较旧的节点上计时bar.fn ,则会看到速度较慢:

 for (var i = 0; i < 100000000; i++) { bar.fn(); // can't inline here } 

正因为它使用隐藏的类,不允许优化编译器内联bar.fn调用。

现在最后需要注意的是,这个基准testing并不测量函数调用的性能,而是衡量优化编译器是否可以通过在内部调用内部函数来将此循环减less到空循环。

对象文字通过结构IE以相同的顺序共享隐藏类(“v8内部术语”),即使构造函数初始化它们到完全相同的字段,由不同的构造函数创build的对象也将具有不同的隐藏类。

在生成foo.fn()代码时,在编译器中,您通常不能访问特定的foo对象,只能访问其隐藏的类。 从隐藏类可以访问fn函数,但是因为共享隐藏类在fn属性中实际上可能具有不同的function,所以这是不可能的。 所以,因为在编译时不知道哪个函数会被调用,所以你不能内联这个调用 。

如果使用跟踪内联标志运行代码:

 $ /c/etc/iojs.exe --trace-inlining test.js t: 651ms 

但是,如果您更改了任何内容,那么.fn始终是相同的函数,或者foobar具有不同的隐藏类:

 $ /c/etc/iojs.exe --trace-inlining test.js Inlined foo.fn called from . t: 88ms 

(我在bar.asd = 3 之前通过做bar.asd = 3来做到这一点,但是有很多不同的方法来实现它,比如构build器和原型,你肯定知道这些是高性能的方法)

要查看版本之间的变化,请运行以下代码:

 var foo = {}; foo.fn = function() {}; var bar = {}; bar.fn = function() {}; foo.fn(); console.log("foo and bare share hidden class: ", %HaveSameMap(foo, bar)); 

正如你所看到的,node10和iojs之间的结果是不同的:

 $ /c/etc/iojs.exe --allow-natives-syntax test.js foo and bare share hidden class: true $ node --allow-natives-syntax test.js foo and bare share hidden class: false 

我最近没有详细地跟踪v8的发展,所以我不能指出确切的原因,但这些启发式一直在变化。

IE11是封闭源代码,但是从他们所logging的所有内容来看,它看起来好像和v8非常相似。