【问题标题】:JsPerf: ParseInt vs Plus conversionJsPerf:ParseInt 与 Plus 转换
【发布时间】:2015-04-12 00:21:31
【问题描述】:

我尝试使用以下 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

有人能解释一下结果吗?

谢谢

【问题讨论】:

  • 如果核心代码在这里,而不是通过链接引用,您将在此处获得更好的响应(我给了您+1 BTW,因为这是一个有趣的问题)。
  • 一个有趣的问题,但更多的是关于 jsperf 和测试用例,而不是 parseInt+。我认为你的标题有点误导。
  • 我编辑了这个问题。我希望现在更好
  • 没人知道为什么会这样?

标签: javascript performance type-conversion parseint


【解决方案1】:

在第二种情况下,+ 更快,因为在这种情况下,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 会调用它来测量您的操作是否有一定的迭代次数n。这个过程重复了几次:生成一个新函数,调用它来收集一个测量样本。在样本之间调整迭代次数,以确保函数运行足够长的时间以提供有意义的测量。

这里需要注意的重要一点是

  1. 你的 case 和 Benchmark.prototype.setup 都是文本内联的;
  2. 您要测量的操作有一个循环;

本质上我们讨论了为什么下面的代码带有一个局部变量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 入口块来完成的。

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

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

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

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

稍后的 LCM 将只执行此操作并将其移出循环,而不会对循环本身感兴趣。这成为可能,因为现在 V8 知道 * 的两个操作数都是原语 - 所以可能没有副作用。

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

这也意味着第二版基准测试实际上并没有测量您希望它测量的内容,而第一版确实掌握了parseInt(x)+x 性能之间的一些差异,这更多是靠运气:您在 V8 的当前优化管道(曲轴)中遇到了一个限制,导致它无法吃掉整个微基准测试。

【讨论】:

  • ...哇。从方格纸流程图到复杂的 OSR V8 SSAIR 优化图。这似乎是一个简单的问题,但这是我见过的最复杂的答案之一。 +1
【解决方案2】:

我相信原因是因为 parseInt 寻找的不仅仅是转换为整数。它还会像解析像素值时一样从字符串中删除任何剩余的文本:

var width = parseInt(element.style.width);//return width as integer

而加号无法处理这种情况:

var width = +element.style.width;//returns NaN

加号执行从字符串到数字的隐式转换,并且仅进行该转换。 parseInt 尝试首先从字符串中理解(比如用度量标记的整数)。

【讨论】:

  • 问题是为什么在第一种情况下 parseInt 接缝比 plus
猜你喜欢
  • 2017-03-15
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2020-12-22
  • 2015-08-04
  • 1970-01-01
  • 2018-06-11
  • 2012-09-26
相关资源
最近更新 更多