对不同对象中的函数使用相同的键时,V8 中的函数调用缓慢
Posted
技术标签:
【中文标题】对不同对象中的函数使用相同的键时,V8 中的函数调用缓慢【英文标题】:slow function call in V8 when using the same key for the functions in different objects 【发布时间】:2015-03-27 21:05:44 【问题描述】:也许不是因为调用慢,而是查找慢;我不确定,但这里有一个例子:
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');
在win8.1上测试
firefox 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
:
最近 v8 出了什么问题?这是什么原因造成的?
【问题讨论】:
我在 Chrome 中得到了相同的结果。没在IE里测试,但是真的要34秒吗? 是的,但它第一次崩溃。即可爱:) 同样奇怪的是,io.js 使用更新版本的引擎比 node.js 慢得多,而 chrome 比使用几乎相同引擎的 node.js 慢得多。跨度> 我的猜测(完全任意)是这与在同一范围内具有相似结构的两个动态(ish)对象有关,但不能保证相同,从而导致问题the caches used for monomorphism 并导致一个查找被取消优化。不过,这完全是不可能的。对实际答案非常感兴趣。 不管嵌套多少,foo.one.fn
和 bar.two.fn
都会导致相同的延迟
【参考方案1】:
对象字面量通过结构 I.E. 共享隐藏类(v8 内部术语中的“映射”)。相同的命名键以相同的顺序,而对象 即使构造函数将它们初始化为完全相同的字段,从不同构造函数创建的隐藏类也会有所不同。
在为foo.fn()
生成代码时,在编译器中您通常无法访问特定的foo
对象,而只能访问其隐藏类。从隐藏类中,您可以访问 fn
函数,但因为
共享隐藏类实际上可以在 fn
属性处具有不同的功能,这是不可能的。所以因为你在编译时不知道会调用哪个函数,所以你不能inline the call。
如果你运行带有跟踪内联标志的代码:
$ /c/etc/iojs.exe --trace-inlining test.js
t: 651ms
但是,如果您更改任何内容以使 .fn
始终是相同的功能,或者 foo
和 bar
具有不同的隐藏类:
$ /c/etc/iojs.exe --trace-inlining test.js
Inlined foo.fn called from .
t: 88ms
(我通过在bar.fn
-assignment 之前执行bar.asd = 3
before 来做到这一点,但是有很多不同的方法可以实现它,例如您肯定知道的构造函数和原型高性能javascript的方法)
要查看版本之间的变化,请运行以下代码:
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 是封闭源代码,但从他们记录的所有内容来看,它实际上似乎与 v8 非常相似。
【讨论】:
【参考方案2】:第一基础。
V8 使用 hidden classes 与 transitions 连接来发现蓬松无形的 JavaScript 对象中的静态结构。
隐藏类描述对象的结构,转换将隐藏类链接在一起,描述如果对对象执行特定操作,应使用哪个隐藏类。
例如,下面的代码将导致下面的隐藏类链:
var o1 = ;
o1.x = 0;
o1.y = 1;
var o2 = ;
o2.x = 0;
o2.y = 0;
此链是在您构造 o1
时创建的。当o2
被构造时,V8 简单地遵循已建立的转换。
现在,当使用属性fn
存储函数时,V8 会尝试给这个属性一个特殊的处理:而不是仅仅在隐藏类中声明对象包含属性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 内部不使用它们的名称,而是使用它们的 identity。
因为 V8 无法遵循这种转换,V8 将决定其将功能提升到隐藏类的决定是不正确且浪费的。图片会变的
V8 将创建一个新的隐藏类,其中fn
只是一个简单的属性,不再是一个常量函数属性。它将重新路由转换并标记旧的转换目标已弃用。请记住,o1
仍在使用它。但是下一次代码触及o1
,例如当从中加载一个属性时 - 运行时会将o1
从已弃用的隐藏类中迁移出来。这样做是为了减少多态性——我们不希望 o1
和 o2
有不同的隐藏类。
为什么在隐藏类上有函数很重要?因为这提供了 V8 的优化编译器信息,它用于内联方法调用。如果调用目标存储在隐藏类本身上,它只能内联方法调用。
现在让我们将这些知识应用到上面的示例中。
因为转换 bar.fn
和 foo.fn
成为普通属性之间存在冲突 - 函数直接存储在这些对象上,而 V8 无法内联 foo.fn
的调用导致性能下降。
之前可以内联调用吗? 是的。变化如下:在较旧的 V8 中没有弃用机制,所以即使在我们发生冲突并重新路由 fn
转换后,foo
也没有迁移到 fn
成为正常财产。相反,foo
仍然保留隐藏类,其中 fn
是一个常量函数属性,直接嵌入到隐藏类中,允许优化编译器内联它。
如果您尝试在旧节点上计时 bar.fn
,您会发现它更慢:
for (var i = 0; i < 100000000; i++)
bar.fn(); // can't inline here
正是因为它使用了不允许优化编译器来内联bar.fn
调用的隐藏类。
最后要注意的是,这个基准测试并不衡量函数调用的性能,而是衡量优化编译器是否可以通过内联调用来将此循环减少为空循环。
【讨论】:
@Esailija 我不认为你有什么问题——在高层次上,你的答案是准确的。当我看到你回答时,我想停止打字——但我已经在中间了,所以我决定如果我停下来,那就太浪费了。 (如果我们深入挖掘时间:过去的情况是 V8 没有将转换附加到根Object
隐藏类,foo.fn
和 bar.fn
都会很快)
多态性解决方案是否能够内联多个(两个)类的调用,或者根本不敢内联它们?
@Bergi 1) 如果它可以从隐藏类本身确定目标,它将内联 - 在上面的示例中,虽然没有多态调用,所以我不确定你是哪个特定代码 sn-p指的是。 2) 是的,确实如此——正如所提到的使用函数标识(想想===
)——这在某种程度上是当前设计的一个缺点。理想情况下不应该。
我也过着你的草图...有没有可能来自其他人可以使用的应用程序/网站?
@sgunliffe 本质上与我在mrale.ph/blog/2012/11/25/shaky-diagramming.html中描述的库相同以上是关于对不同对象中的函数使用相同的键时,V8 中的函数调用缓慢的主要内容,如果未能解决你的问题,请参考以下文章