Javascript 对不同对象中的函数使用相同的键时,V8中的函数调用速度较慢

Javascript 对不同对象中的函数使用相同的键时,V8中的函数调用速度较慢,javascript,node.js,performance,google-chrome,v8,Javascript,Node.js,Performance,Google Chrome,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'); var foo={}; foo.fn=函数(){}; var bar={

可能不是因为呼叫速度慢,而是查找速度慢;我不确定,但这里有一个例子:

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');
var foo={};
foo.fn=函数(){};
var bar={};
bar.fn=函数(){};
时间('t');
对于(变量i=0;i<100000000;i++){
foo.fn();
}
控制台。时间结束('t');
在win8.1上测试

  • firefox 35.01:~240ms
  • chrome 40.0.2214.93(V8 3.30.33.15):~760ms
  • msie 11:34秒
  • 节点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
  • 节点0.10.21(V8 3.14.5.9):~100ms
  • iojs 1.0.4(V8 4.1.0.12):~100ms

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

对象文字按结构共享隐藏类(v8内部术语中的“映射”),即相同顺序的相同命名键,而对象 从不同的构造函数创建的类将具有不同的隐藏类,即使构造函数将它们初始化为完全相同的字段

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

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

$ /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
-赋值之前执行
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使用与转换连接的隐藏类来发现蓬松的不成形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=函数fff(){};

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

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

因为V8无法遵循此转换,所以V8将决定将函数升级到隐藏类的决定是不正确和浪费的。情况将会改变

V8将创建一个新的隐藏类,其中
fn
只是一个简单的属性,不再是常量函数属性。它将重新路由转换,并将旧的转换目标标记为已弃用。请记住,
o1
仍在使用它。但是,下次代码触及
o1
时,例如从中加载属性时,运行时将从不推荐使用的隐藏类中迁移
o1
。这样做是为了减少多态性-我们不希望
o1
o2
有不同的隐藏类

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

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

因为在转换
bar.fn
foo.fn
之间存在冲突,这将成为正常属性-函数直接存储在这些对象上,V8无法内联调用
foo.fn
,导致性能降低

它能在打电话之前打进来吗。这里是改变的地方:在旧版本的V8中没有弃用机制,因此即使在发生冲突并重新路由
fn
转换之后,
foo
也没有迁移到隐藏类中,
fn
成为一个正常的类