我尝试使用以下 jsperf 来探测 plus (+) 转换比 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 会调用它来测量特定迭代次数的操作
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 的当前优化管道 (Crankshaft) 中遇到了限制,阻止它吞噬整个微基准测试。
关于javascript - JsPerf : ParseInt vs Plus conversion,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/28457585/