我尝试使用以下jsperf探测加(+)转换比parseInt更快,结果令我惊讶:

Parse vs Plus

准备代码

<script>
  Benchmark.prototype.setup = function() {
    var x = "5555";
  };
</script>

解析样本
var y = parseInt(x); //<---80 million loops

加样
var y = +x; //<--- 33 million loops

原因是因为我使用“Benchmark.prototype.setup”来声明我的变量,但是我不明白为什么

请参阅第二个示例:

Parse vs Plus (local variable)
<script>
  Benchmark.prototype.setup = function() {
    x = "5555";
  };
</script>

解析样本
var y = parseInt(x); //<---89 million loops

加样
var y = +x; //<--- 633 million loops

有人可以解释结果吗?

谢谢

最佳答案

在第二种情况下,+更快,因为在那种情况下,V8实际上将其移出了基准循环,从而使基准循环为空

这是由于当前优化管道的某些特性而发生的。但是在深入探讨细节之前,我想提醒一下Benchmark.js的工作原理。

要衡量您编写的测试用例,它需要使用您还提供的Benchmark.prototype.setup和测试用例本身,并动态生成一个看起来像近似为的函数,如下所示(我正在跳过一些无关的细节):

function (n) {
  var start = Date.now();

  /* Benchmark.prototype.setup body here */
  while (n--) {
    /* test body here */
  }

  return Date.now() - start;
}

创建该函数后,Benchmark.js就会调用该函数来测量您的op的迭代次数n。重复此过程几次:生成一个新函数,调用它以收集测量样本。在样本之间调整迭代次数,以确保函数运行足够长的时间以进行有意义的测量。

这里要注意的重要事项是
  • 您的案例和Benchmark.prototype.setup均以文本形式内联;
  • 您要测量的操作周围有一个循环;

  • 本质上,我们讨论了为什么下面的代码带有局部变量x
    function f(n) {
      var start = Date.now();
    
      var x = "5555"
      while (n--) {
        var y = +x
      }
    
      return Date.now() - start;
    }
    

    运行速度比带有全局变量x的代码慢

    function g(n) {
      var start = Date.now();
    
      x = "5555"
      while (n--) {
        var y = +x
      }
    
      return Date.now() - start;
    }
    

    (注意:这种情况在问题本身中称为局部变量,但事实并非如此, x是全局)

    当使用足够大的n值(例如f(1e6))执行这些函数时,会发生什么?

    当前的优化管道以特殊方式实现OSR。它不会生成优化代码的OSR特定版本并在以后将其丢弃,而是生成一个可用于OSR和常规输入的版本,如果我们需要在同一循环中执行OSR,甚至可以重用该版本。这是通过将特殊的OSR入口模块注入(inject)控制流程图中的正确位置来完成的。

    在构建该功能的SSA IR时,将注入(inject)OSR入口块,它会急切地将所有局部变量从传入的OSR状态中复制出来。结果,V8无法看到本地x实际上是一个常量,甚至失去了有关其类型的任何信息。对于后续的优化传递,x2看起来可以是任何东西。

    由于x2可以是任何表达式,因此+x2也可以具有任意副作用(例如,它可以是附加了valueOf的对象)。这样可以防止循环不变代码运动传递将+x2移出循环。

    为什么g比起更快? V8在这里发挥了作用。它跟踪包含常量的全局变量:在此基准测试中,全局x始终包含"5555",因此V8只是用其值替换x访问,并将此优化的代码标记为取决于x的值。如果有人用不同于所有依赖代码的方式替换x值,则将取消优化。全局变量也不是OSR状态的一部分,并且不参与SSA重命名,因此V8不会被合并OSR和正常进入状态的“虚假”φ函数所混淆。这就是为什么当V8优化g时最终会在循环主体中生成以下IR(左侧的红色条纹显示了循环):

    注意:+x被编译为x * 1,但这只是实现细节。

    以后的LICM只会执行此操作并将其移出循环,而不会在循环本身中留下任何兴趣。之所以可行,是因为现在V8知道*的两个操作数都是基元-因此可能存在而没有副作用。

    这就是g更快的原因,因为空循环显然比非空循环快。

    这也意味着基准测试的第二个版本实际上并没有测量您想要测量的值,而第一个版本却确实掌握了parseInt(x)+x性能之间的一些差异,这更多的是靠运气:您遇到了V8的限制当前的优化管道(曲轴)阻止了它耗尽整个微基准。

    07-26 01:36