我对这个simple jsperf test:
的结果感到惊讶Benchmark.prototype.setup = function() {
var O = function() {
this.f = function(){};
}
var o = new O();
var o2 = {
f : function(){}
};
};
// Test case #1
o.f(); // ~721M ops/s
// Test case #2
o2.f(); // ~135M ops/s
我希望两者都执行相同的操作(实际上Firefox的性能类似)。 V8必须优化案例#1,但是什么?
答案 0 :(得分:25)
关于V8和jsPerf的第一个基础知识:
V8使用一种名为隐藏类的技术。每个隐藏类描述某个对象形状,例如对象在偏移x
或对象具有方法16
时具有属性f
,并且这些隐藏类通过过渡连接在一起因为对象被突变形成过渡树(严格来说是dags)。并非所有隐藏类都驻留在同一个转换树中;相反,每个构造函数都会生成一个新的过渡树。看看these slides,了解隐藏课程背后的基本思想。
当jsPerf执行以下操作以运行测试时:给定setup
和body
多次会生成并运行一个类似于此的函数:
function measure() {
/* setup */
var start = Date.now();
for (var i = 0; i < N; i++) {
/* body */
}
var end = Date.now();
/* N / (start - end) determines ops / ms reported */
}
每次运行都称为样本。
现在让我们看一下基准测试中的过渡树。
o
的隐藏类属于转换树,在构造函数O
中具有根。请注意,每次构造函数都会被调用一次。这允许V8在内存中构建以下转换树:
O{} -f-> O{ f: <closure> }
隐藏的o
类基本上告诉V8 o
有一个名为f
的方法,由给定的闭包实现。这允许V8的优化编译器在您的基准测试中内联f
,这实际上使得基准测试循环为空。
隐藏的o2
类属于Object
的过渡树。首先请注意setup
被多次调用,因此如果V8尝试将相同的优化应用于促销f
的方法,它将会到达不可能的转换树:
Object{} -f-> Object{ f: <closure> }
-f-> Object{ f: <other closure> }
事实上,V8甚至没有尝试过。 V8实现者预见到了这种情况,V8只使f
成为正常属性:
Object{} -f-> Object{ f: <property at offset 8> }
因此,要调用o2.f()
,首先需要加载它,这也会损害内联。
在这里你应该意识到一件重要的事情:如果你两次调用O
构造函数,那么V8将到达V8避免命中Object
的同一个不可能的转换树:
O{} -f-> O{ f: <closure> }
-f-> O{ f: <other closure> }
你不能拥有这样的结构。在这种情况下,V8即时将f
转换为字段而不是使其成为方法并重写转换树:
O{} -f-> O{ f: <property at offset 8> }
在http://jsperf.com/function-call-on-js-objects/3中看到此效果,我在创建new O()
之前添加了一个o
。您会注意到使用new
构造的对象文字和对象的性能是相同的。
此处的另一个细节是,如果在全局范围中声明了文字,V8也会尝试将f
转换为文字的方法。针对V8,请参阅http://jsperf.com/function-call-on-js-objects/5和Issue 2246。这里的推理很简单:全局范围内的文字只被评估一次,因此这种促销很可能会成功,并且如果多次评估文字,那么方法之间就不会发生冲突。
您可以在my blog post中了解有关类似问题的更多信息。
答案 1 :(得分:2)
V8对已知原型进行了优化。换句话说,通过new
对对象的使用和创建进行了优化。
您可以编写更多类似的测试,这将始终是结论。
在第二种情况下,你会使发动机失明。它不知道为什么,o2
是否或何时会有属性。