为什么新电脑变慢了?

31

基准测试:

JsPerf

不变量:

var f = function() { };

var g = function() { return this; }

测试:

以下按照预期速度的顺序

  • new f;
  • g.call(Object.create(Object.prototype));
  • new (function() { })
  • (function() { return this; }).call(Object.create(Object.prototype));

实际速度:

  1. new f;
  2. g.call(Object.create(Object.prototype));
  3. (function() { return this; }).call(Object.create(Object.prototype));
  4. new (function() { })

问题:

  1. 当你交换内联匿名函数中的 fg 时,为什么使用 new(第4个测试)的效率会更低?

更新:

具体是什么导致使用内联的 fgnew 的效率变慢。

我对 ES5 规范的引用或对 JagerMonkey 或 V8 源代码的引用感兴趣。(如有需要可以链接 JSC 和 Carakan 源代码。对于 IE 团队,如果他们愿意,可以泄露 Chakra 源代码。)

如果您链接任何 JS 引擎源代码,请解释一下。


3
出于好奇,使用Object.create(Object.prototype)的目的是什么,而不是使用对象字面量({})?它们不完全相同吗?这可能是某些性能差异的源头吗? - maerics
@maerics 我觉得 Object.create(object.prototype) 更符合 new 的 "精神"。在 Chrome 中,似乎 {} 更快,在 FF 中更慢,在 IE 中大致相同。 - Raynos
虽然我有点模糊地理解为什么Object.create(Object.prototype)更符合new的精神,但我很想看到一个构造不只是空对象的示例来加以确认。 - Domenic
@Domenic Benchmark for {} vs Object.create 除了在 Firefox 中的简单情况外,字面量更快。 - Raynos
1
@Alexander 谢谢你的建议。我已经移除了它以使得更加清晰明了。 - Raynos
显示剩余3条评论
5个回答

18
#4 和其他情况的主要区别在于,当你第一次将闭包作为构造函数使用时,它总是非常昂贵的。
  1. 这总是在 V8 运行时处理(而不是在生成的代码中),在编译的 JS 代码和 C++ 运行时之间的转换非常昂贵。随后的分配通常由生成的代码处理。您可以查看 builtins-ia32.cc 中的 Generate_JSConstructStubHelper ,并注意当关闭没有初始映射时它会掉落到 Runtime_NewObject。(参见 http://code.google.com/p/v8/source/browse/trunk/src/ia32/builtins-ia32.cc#138
  2. 当第一次将闭包用作构造函数时,V8 必须创建一个新的映射(也称为隐藏类,并将其分配为该闭包的初始映射。请参见 http://code.google.com/p/v8/source/browse/trunk/src/heap.cc#3266。重要的是,映射分配在一个单独的内存空间中。该空间不能被快速部分扫描收集器清除。当映射空间溢出时,V8 必须执行相对昂贵的完全标记-清除GC。

在第一次将闭包用作构造函数时会发生其他几件事情,但1和2是测试用例#4缓慢的主要贡献者。

如果我们将表达式#1和#4进行比较,则差异为:

  • #1 不会每次分配一个新的闭包;
  • #1 不会每次进入运行时:在 closure 获取初始映射之后,构建处理将在生成代码的快速路径中处理。在生成的代码中处理整个构建比在运行时和生成的代码之间来回转换要快得多。
  • #1 不会每次为新的闭包分配一个新的初始映射;
  • #1 不会通过溢出映射空间引起标记清除 (只有廉价扫描)。

如果我们比较#3和#4,那么它们之间的区别是:

  • #3 不会每次为新的闭包分配一个新的初始映射;
  • #3 不会通过溢出映射空间引起标记清除 (只有廉价扫描);
  • #4 在JS方面做得更少(没有Function.prototype.call,没有Object.create,没有Object.prototype查找等),而在C++方面做得更多(#3每次执行Object.create时也会进入运行时,但在那里做得很少)。

底线是,第一次将闭包用作构造函数与后续相同闭包的构造函数调用相比是昂贵的,因为V8必须设置一些管道。如果我们立即丢弃闭包,我们基本上会丢掉V8加速后续构造函数调用所做的所有工作。


5
问题在于你可以检查各种引擎的当前源代码,但这并没有什么帮助。不要试图聪明地超越编译器。它们会尝试为最常见的用法进行优化。我认为 (function() { return this; }).call(Object.create(Object.prototype)) 被调用1,000次根本没有真正的用例。

"程序应该为人们编写,只是偶然地为机器执行."

Abelson&Sussman,SICP,第一版前言


我知道在几乎所有情况下,newObject.create更快。但我很好奇为什么在那种特殊情况下不是这样。我的意思不是说我会根据答案使用更多或更少的new。我写易读易维护的代码。 - Raynos
是的,但如果你没有真正的用例或性能问题,那又有什么意义呢?我的意思是,你也不应该使用 new (function(){}) 在运行时动态创建 1,000 个函数。将可用模式与不可用模式进行比较并没有真正的好处。 - gblazex
2
好奇心总是让我失去理智。 - Raynos

3
我想下面的扩展解释了V8中正在发生的事情:
  1. t(exp1):t(Object Creation)
  2. t(exp2):t(通过Object.create()创建对象)
  3. t(exp3):t(通过Object.create()创建对象) + t(Function对象创建)
  4. t(exp4):t(Object Creation) + t(Function对象创建) + t(Class对象创建)[在Chrome中]

    • 要查看Chrome中的隐藏类,请访问:http://code.google.com/apis/v8/design.html
    • 当通过Object.create()创建新对象时,不需要创建新的Class对象。已经有一个用于对象字面量的Class对象,因此不需要新的Class对象。

Chrome 在其 JS 引擎中创建了类(对用户隐藏),我建议您参考他们的文档。我已经更新了我的答案。 - salman.mirghasemi
2
是的,这些观察是正确的。你要找的代码在这里:http://code.google.com/p/v8/source/browse/trunk/src/heap.cc#3269 这里还有一件重要的事情需要知道,就是隐藏类(在V8源代码中称为映射)是在单独的内存空间中分配的。这个空间不能被部分(scavenge)收集器清理。因此,当你分配大量的映射时,你开始得到昂贵的完整收集(标记-扫描),而不是廉价的scavenges。这就是为什么t(exp1)和t(exp4)之间有如此大的差异。你为垃圾回收付出了比其他情况下更多的代价。 - Vyacheslav Egorov
@gsnedders 咬紧牙关,写下解释 :D - Raynos
@Raynos:函数在第一次尝试使用给定函数构造对象时才会惰性地获取初始映射。因此,在exp3中,您的闭包不会获得初始映射,在exp4中它们会获得它们(每个闭包都会获得一个新的独特映射)。创建初始映射确实非常便宜,但是映射空间溢出(当您执行此代码片段数十万次时会发生)会导致昂贵的(与清理相比)标记扫描收集。这就是差异所在。所有其他测试用例都不会溢出映射空间,也不会导致标记扫描GC。 - Vyacheslav Egorov
@VyacheslavEgorov 请将其回答为答案,以便我接受:) - Raynos
显示剩余8条评论

0

嗯,这两个调用并不完全相同。考虑以下情况:

var Thing = function () {
  this.hasMass = true;
};
Thing.prototype = { holy: 'object', batman: '!' };
Thing.prototype.constructor = Thing;

var Rock = function () {
  this.hard = 'very';
};
Rock.prototype = new Thing();
Rock.constructor = Rock;

var newRock = new Rock();
var otherRock = Object.create(Object.prototype);
Rock.call(otherRock);

newRock.hard // => 'very'
otherRock.hard // => 'very'
newRock.hasMass // => true
otherRock.hasMass // => undefined
newRock.holy // => 'object'
otherRock.holy // => undefined
newRock instanceof Thing // => true
otherRock instanceof Thing // => false

因此,我们可以看到调用Rock.call(otherRock)并不会导致otherRock从原型中继承。这至少解释了一部分额外的缓慢。尽管在我的测试中,即使在这个简单的例子中,new构造函数也慢了近30倍。


当然不是这样的。你必须调用 Object.create(Rock.prototype),因为在创建对象之前我调用了 Object.prototype - Raynos
哦,这样就更有意义了。抱歉,我没有仔细阅读! - benekastah
е—Ҝ...зҺ°еңЁжҲ‘жғізҹҘйҒ“Object.create()жңүд»Җд№ҲдёҚеҗҢпјҹ - benekastah
“不要过度优化”,因为使用“new”更多,因此优化它会带来更高的感知速度增加收益。 - Raynos

0
new f;
  1. 使用本地函数'f'(通过本地框架中的索引访问)- 成本低廉。
  2. 执行字节码BC_NEW_OBJECT(或类似的操作)- 成本低廉。
  3. 执行函数 - 此处成本低廉。

现在是这样:

g.call(Object.create(Object.prototype));
  1. 找到全局变量 Object - 便宜?
  2. 在 Object 中查找属性 prototype - 一般般
  3. 在 Object 中查找属性 create - 一般般
  4. 找到本地变量 g; - 便宜
  5. 查找属性 call - 一般般
  6. 调用 create 函数 - 一般般
  7. 调用 call 函数 - 一般般

还有这个:

new (function() { })
  1. 创建新的函数对象(即匿名函数)- 相对昂贵。
  2. 执行字节码 BC_NEW_OBJECT - 廉价
  3. 执行函数 - 此处廉价。

如您所见,情况#1是最不耗费资源的。


真正有趣的是 #1 比 #2 更快。但是将 f 和 g 替换为匿名函数使 #3 比 #4 更快,这一点我不理解。 - Raynos
毫不意外,g.call(Object.create(Object.prototype)); 这行代码执行了相当多的操作。这里有两个函数调用,两个额外的属性查找以及全局 Object 查找。你可以尝试在本地变量中缓存 Object.prototype 的结果。 - c-smile

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接