当对不同对象中的函数使用相同的键时,V8 中的函数调用缓慢
slow function call in V8 when using the same key for the functions in different objects
可能不是调用慢,而是查找慢;我不确定,但这里有一个例子:
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上测试
- 火狐 35.01: ~240ms
- chrome 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
:
- chrome 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 出了什么问题?这是什么原因造成的?
对象字面量按结构 I.E. 共享隐藏 class("map" in v8 内部术语)相同顺序的相同命名键,而对象
从不同的构造函数创建将有不同的隐藏 class,即使构造函数将它们初始化为完全相同的字段。
为 foo.fn()
生成代码时,在编译器中您通常无法访问特定的 foo
对象,只能访问其隐藏的 class。从隐藏的 class 你可以访问 fn
函数但是因为
共享隐藏 class 实际上可以在 fn
属性 具有不同的功能,这是不可能的。所以因为你不知道在编译时会调用哪个函数,所以你不能 inline the call.
如果您 运行 带有跟踪内联标志的代码:
$ /c/etc/iojs.exe --trace-inlining test.js
t: 651ms
但是,如果您更改任何内容,使 .fn
始终具有相同的功能,或者 foo
和 bar
具有不同的隐藏 class:
$ /c/etc/iojs.exe --trace-inlining test.js
Inlined foo.fn called from .
t: 88ms
(我通过在 bar.fn
赋值 bar.asd = 3
之前 来做到这一点,但是有很多不同的方法可以实现它,例如您肯定知道的构造函数和原型是实现高性能的途径 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 非常相似。
第一基础知识。
V8 使用 hidden classes 与 transitions 相连接来发现蓬松无形中的静态结构 JavaScript对象。
hidden classes 描述了对象的结构,transitions linkhidden classes 一起描述了如果执行某个动作应该使用哪个hidden class在对象上。
例如,下面的代码将导致以下隐藏 classes 链:
var o1 = {};
o1.x = 0;
o1.y = 1;
var o2 = {};
o2.x = 0;
o2.y = 0;
这条链是在您构造 o1
时创建的。当构建 o2
时,V8 简单地遵循已建立的转换。
现在,当 属性 fn
用于存储函数时,V8 会尝试对 属性 进行特殊处理:而不是仅在隐藏 class 中声明该对象包含一个 属性 fn
V8 puts 函数到隐藏的 class。
var o = {};
o.fn = function fff() { };
现在这里有一个有趣的结果:如果您将不同的函数存储到具有相同名称的字段中,V8 将无法再简单地跟随转换,因为函数的值 属性 与预期值不匹配:
var o1 = {};
o1.fn = function fff() { };
var o2 = {};
o2.fn = function ggg() { };
在评估 o2.fn = ...
赋值时,V8 会看到有一个标记为 fn
的转换,但它会导致不合适的隐藏 class:它包含 fff
在 fn
属性 中,而我们正在尝试存储 ggg
。注意:我给出函数名称只是为了简单起见——V8 内部不使用它们的名称,而是使用它们的 identity.
因为 V8 无法遵循此转换,所以 V8 将决定将函数提升到隐藏 class 的决定是不正确和浪费的。图片会变
V8 将创建一个新的隐藏 class,其中 fn
只是一个简单的 属性 而不是常量函数 属性。它将重新路由转换并将旧的转换目标标记为 已弃用 。请记住 o1
仍在使用它。但是下次代码触及 o1
例如当从中加载 属性 时 - 运行时会将 o1
从已弃用的隐藏 class 中迁移出来。这样做是为了减少多态性——我们不希望 o1
和 o2
有不同的隐藏 classes.
为什么在隐藏的 classes 上设置函数很重要?因为这为 V8 提供了用于 内联方法调用 的优化编译器信息。如果调用目标存储在隐藏 class 本身,它只能内联方法调用。
现在让我们将这些知识应用到上面的例子中。
因为转换 bar.fn
和 foo.fn
成为正常属性之间存在冲突 - 函数直接存储在这些对象上并且 V8 无法内联 foo.fn
的调用导致性能较慢。
它可以内联之前的调用吗? 是。这是变化的地方:在旧的 V8 中 没有弃用机制 所以即使我们发生冲突并重新路由 fn
转换,foo
也没有迁移到隐藏class 其中 fn
变为正常的 属性。相反 foo
仍然保留隐藏 class 其中 fn
是一个常量函数 属性 直接嵌入到隐藏 class 允许优化编译器内联它。
如果您尝试在旧节点上计时 bar.fn
,您会发现它更慢:
for (var i = 0; i < 100000000; i++) {
bar.fn(); // can't inline here
}
正是因为它使用了隐藏的class,不允许优化编译器内联bar.fn
调用。
这里最后要注意的是,该基准测试不衡量函数调用的性能,而是衡量优化编译器是否可以通过内联调用将循环减少为空循环。
可能不是调用慢,而是查找慢;我不确定,但这里有一个例子:
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上测试
- 火狐 35.01: ~240ms
- chrome 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
:
- chrome 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 出了什么问题?这是什么原因造成的?
对象字面量按结构 I.E. 共享隐藏 class("map" in v8 内部术语)相同顺序的相同命名键,而对象 从不同的构造函数创建将有不同的隐藏 class,即使构造函数将它们初始化为完全相同的字段。
为 foo.fn()
生成代码时,在编译器中您通常无法访问特定的 foo
对象,只能访问其隐藏的 class。从隐藏的 class 你可以访问 fn
函数但是因为
共享隐藏 class 实际上可以在 fn
属性 具有不同的功能,这是不可能的。所以因为你不知道在编译时会调用哪个函数,所以你不能 inline the call.
如果您 运行 带有跟踪内联标志的代码:
$ /c/etc/iojs.exe --trace-inlining test.js
t: 651ms
但是,如果您更改任何内容,使 .fn
始终具有相同的功能,或者 foo
和 bar
具有不同的隐藏 class:
$ /c/etc/iojs.exe --trace-inlining test.js
Inlined foo.fn called from .
t: 88ms
(我通过在 bar.fn
赋值 bar.asd = 3
之前 来做到这一点,但是有很多不同的方法可以实现它,例如您肯定知道的构造函数和原型是实现高性能的途径 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 非常相似。
第一基础知识。
V8 使用 hidden classes 与 transitions 相连接来发现蓬松无形中的静态结构 JavaScript对象。
hidden classes 描述了对象的结构,transitions linkhidden classes 一起描述了如果执行某个动作应该使用哪个hidden class在对象上。
例如,下面的代码将导致以下隐藏 classes 链:
var o1 = {};
o1.x = 0;
o1.y = 1;
var o2 = {};
o2.x = 0;
o2.y = 0;
这条链是在您构造 o1
时创建的。当构建 o2
时,V8 简单地遵循已建立的转换。
现在,当 属性 fn
用于存储函数时,V8 会尝试对 属性 进行特殊处理:而不是仅在隐藏 class 中声明该对象包含一个 属性 fn
V8 puts 函数到隐藏的 class。
var o = {};
o.fn = function fff() { };
现在这里有一个有趣的结果:如果您将不同的函数存储到具有相同名称的字段中,V8 将无法再简单地跟随转换,因为函数的值 属性 与预期值不匹配:
var o1 = {};
o1.fn = function fff() { };
var o2 = {};
o2.fn = function ggg() { };
在评估 o2.fn = ...
赋值时,V8 会看到有一个标记为 fn
的转换,但它会导致不合适的隐藏 class:它包含 fff
在 fn
属性 中,而我们正在尝试存储 ggg
。注意:我给出函数名称只是为了简单起见——V8 内部不使用它们的名称,而是使用它们的 identity.
因为 V8 无法遵循此转换,所以 V8 将决定将函数提升到隐藏 class 的决定是不正确和浪费的。图片会变
V8 将创建一个新的隐藏 class,其中 fn
只是一个简单的 属性 而不是常量函数 属性。它将重新路由转换并将旧的转换目标标记为 已弃用 。请记住 o1
仍在使用它。但是下次代码触及 o1
例如当从中加载 属性 时 - 运行时会将 o1
从已弃用的隐藏 class 中迁移出来。这样做是为了减少多态性——我们不希望 o1
和 o2
有不同的隐藏 classes.
为什么在隐藏的 classes 上设置函数很重要?因为这为 V8 提供了用于 内联方法调用 的优化编译器信息。如果调用目标存储在隐藏 class 本身,它只能内联方法调用。
现在让我们将这些知识应用到上面的例子中。
因为转换 bar.fn
和 foo.fn
成为正常属性之间存在冲突 - 函数直接存储在这些对象上并且 V8 无法内联 foo.fn
的调用导致性能较慢。
它可以内联之前的调用吗? 是。这是变化的地方:在旧的 V8 中 没有弃用机制 所以即使我们发生冲突并重新路由 fn
转换,foo
也没有迁移到隐藏class 其中 fn
变为正常的 属性。相反 foo
仍然保留隐藏 class 其中 fn
是一个常量函数 属性 直接嵌入到隐藏 class 允许优化编译器内联它。
如果您尝试在旧节点上计时 bar.fn
,您会发现它更慢:
for (var i = 0; i < 100000000; i++) {
bar.fn(); // can't inline here
}
正是因为它使用了隐藏的class,不允许优化编译器内联bar.fn
调用。
这里最后要注意的是,该基准测试不衡量函数调用的性能,而是衡量优化编译器是否可以通过内联调用将循环减少为空循环。