我尝试使用以下jsperf来探测加号(+)转换比parseInt更快,结果让我感到惊讶:
准备代码
<script>
Benchmark.prototype.setup = function() {
var x = "5555";
};
</script>
解析样本
var y = parseInt(x); //<---80 million loops
Plus Sample
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
Plus Sample
var y = +x; //<--- 633 million loops
有人可以解释结果吗?
由于
答案 0 :(得分:31)
在第二种情况下,+
更快,因为在这种情况下,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条目块注入控制流图中的正确位置来完成的。
注入OSR入口块,同时构建函数的SSA IR,并急切地将所有局部变量复制到传入的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)中遇到了一个限制,阻止它吃掉整个微基准线。
答案 1 :(得分:-1)
我相信原因是因为parseInt不只是寻找转换为整数。它还会在解析像素值时删除字符串中的任何剩余文本:
var width = parseInt(element.style.width);//return width as integer
而加号无法处理这种情况:
var width = +element.style.width;//returns NaN
加号执行从字符串到数字的隐式转换以及仅转换。 parseInt首先尝试从字符串中理解(就像用测量标记的整数一样)。