为什么JavaScript中的Math.round比自定义函数慢?

20

我正在尝试创建一个自定义的四舍五入函数,可以将数字四舍五入到任何我想要的间隔。例如(如果我正在处理角度,则会四舍五入到最近的15度)。然后我决定测试一下它与Math.round相比速度如何,并发现它更慢。我在FF8上使用firebug。

function R1(a,b){var c=a%b;return a-c+(c/b+1.5>>1)*b}

function R2(a){return Math.round(a)}

var i,e=1e5;
console.time('1');
i=e;
while(i--){
  R1(3.5,1);
}
console.timeEnd('1');
console.time('2');
i=e;
while(i--){
  R2(3.5);
}
console.timeEnd('2');

我的结果是

1: 464ms
2: 611ms

我以不同的方式运行它们多次,但总是R1的速度更快。也许这只是一个FF浏览器问题,但如果是这样,是什么导致了这种情况。

编辑:我将每个项目从函数调用中取出来,看看会发生什么。

var i,e=1e5,c;
console.time('1');
i=e;
while(i--){
  c=3.5%1;
  3.5-c+(c/1+1.5>>1)*1;
}
console.timeEnd('1');
console.time('2');
i=e;
while(i--){
  Math.round(3.5);
}
console.timeEnd('2');

而且我得到的次数

1: 654ms
2: 349ms

4
考虑发布一个 http://jsperf.com 测试案例。有时,不同的浏览器/版本可能会表现完全不同,因此你看到的可能并不是其他人所看到的 :) - user166390
我的结果:1: 190毫秒 2: 102毫秒。Windows 上使用 Chrome。 - NullUserException
@pst 这个链接显示该网站正在出售。 - qw3n
3
如果将Math.round包装在另一个函数中与直接声明var R2 = Math.round有什么不同?在我的情况下(在OS X上使用Safari),我发现前者可以加快速度,尽管此时R2已经比较快了。 - Lily Ballard
3
这是一个jsperf测试案例链接:http://jsperf.com/comparing-custom-and-bult-in-math-round。根据*Kevin*的建议,将`Math.round`移出了`R2`函数。 - kubetz
显示剩余7条评论
3个回答

18
简短的回答是,在Firefox 8中(但不包括9),Math.round最终调用了一个C++函数,这在JITs中速度较慢。长话短说,情况很复杂,并且在不同版本的Firefox中会有所不同。此外,由于涉及到JITs,不同处理器和操作系统上的结果也会不同。
一些背景知识:根据ECMA-262标准,Math.round将数字四舍五入到最接近的整数,但对于0.5,它会向正无穷方向舍入,而对于[-0.5,-0.0],它会舍入为-0.0(IEEE-754负零)。为了正确实现这一点,Math.round需要比R1更多的操作。它要么需要对舍入为-0的范围进行一些浮点比较(V8这样做),要么从输入中复制符号(SpiderMonkey这样做)。

现在,对于Firefox 8,两个循环都由tracejit编译。对于带有R1的循环,R1被内联并编译为纯本机代码。R2被内联并编译为调用名为js_math_round_impl的C++函数(位于js/src/jsmath.cpp)。

  • 调用任何函数都会增加额外开销,因为需要设置参数、进行调用、推送寄存器等操作。

  • 调用Math.round或类似函数会增加额外开销,因为代码需要验证Math.round仍然是默认的Math.round(即验证没有猴子补丁)。

  • 在JIT中调用C++函数会增加额外开销,因为JIT不知道C++函数使用哪些寄存器,所以编译后的JS函数必须在调用前存储所有调用者保存的寄存器,并在调用后重新加载它们。调用还可能清除其他假设,阻止其他优化。

  • 正如之前提到的,Math.roundR1要做更多工作。

我在JS和C中尝试了几个不同的测试,试图弄清楚调用是否更重要,还是-0检查。结果各不相同,但看起来调用通常是减速的主要原因(占总减速的70-90%)。
在Firefox 9中,使用JM+TI,R1和R2的速度大致相同。在这种情况下,R1再次被内联(我认为),并编译成纯本地代码。对于R2,Math.round由一段jitcode实现,该jitcode直接处理正数,但对于负数(以及NaN等),则调用一个C++函数。因此,对于给定的示例,两者都在jitcode中运行,而R2恰好稍快一些。
一般来说,对于像Math.round这样的函数(传统上是调用C++函数,但足够简单以至于至少某些情况可以直接在jitcode中完成),性能很大程度上取决于引擎实现者为该特定函数进行了多少jitcode优化。

9
读者注意:Dave 是 Mozilla 公司的 JavaScript 大师。 :) - kubetz
@DaveMandelin 感谢您详细的回答。看到所有这些东西如何协同工作以使得这样的事情正常运行是很有趣的。另外,也许我误解了这些 http://jsperf.com/comparing-custom-and-bult-in-math-round/2 的结果,但是 ffb 11.01a 是否比 FF8 运行 R1 慢得多,但运行 Math.round 快得多? - qw3n

8
比较实际上是不正确的。 R2() 是一个函数,它调用了 Math.round()R1() 直接进行舍入。
因此,R2 包含额外的函数调用-这是一种缓慢的操作。
尝试使用相同条件比较舍入实现:
function R1(a,b){var c=a%b;return a-c+(c/b+1.5>>1)*b}
R2 = Math.round;

Credits归功于建议将Math.round()移出R2()的Kevin Ballard。
参见:http://jsperf.com/comparing-custom-and-bult-in-math-round
更新:
Firefox的结果与Chrome非常不同。
注意:我在这个领域没有经验,所以我猜测。如果有经验的人能提供他对这些数字的看法,那就太棒了。
当输入值不变时,Firefox似乎进行了大量优化。它可以这样优化R1(3.5),但是由于JavaScript的动态性质,优化Math.round可能更加困难。Math.round的实现可能在代码执行过程中的任何时刻发生变化。R1()仅使用算术和位运算。使用内置的Math.round(R2()和R3())函数的性能与其他浏览器相当(除了IE 9:o)。

有人有一个好主意,创建了第二个版本的测试用例:

http://jsperf.com/comparing-custom-and-bult-in-math-round/2

这个版本还测试了传递给它们的值正在变化的函数的性能。

你有任何想法为什么Built-in Math.round甚至比静态输入的自定义舍入更有效率吗?


@dzejkej 如果你能从Mozilla那里得到一个答案,那将是很有趣的。我会再等一段时间,看看是否还有其他人能给出答案,如果没有,那么你的回答最接近,我会接受它。 - qw3n
@qw3n 我又更新了我的答案。问题在于我觉得我的答案实际上没有回答那么多——越来越多的问题正在出现:P。我认为目前还不准备关闭。干得好!:) - kubetz
1
@qw3n:实际使用Math.round的性能比Chrome差,但似乎包装函数的值被记忆了。这就解释了为什么函数包装可以加速执行-执行停止发生。我不确定如何在javascript中可靠地完成这项工作,但它看起来很可疑。我必须想象任何寻求效率的人都会手动进行此优化;即从紧密的内部循环中提取常量操作,就像游戏经常做的那样。 - Stefan Kendall
顺便说一下,我尝试使用Math.random修改测试,而且我猜测FF为了速度而牺牲了随机性,因此FF的Math.random比Chrome的实现要快得多。所以我无法获得好的数据。 :/ - Stefan Kendall
1
@dzejkej 绝对同意现在问题比答案多。有趣的是我刚刚在玩耍,突然间我就觉得这没有意义。无论如何,感谢你的工作,也许有人可以找到更多的答案。 - qw3n
显示剩余5条评论

0
我不记得具体的来源了,但它是一段 Google 技术演讲视频,讨论了这个问题。一个属于对象的字段(比如 this.field)比直接引用要慢,因为 Javascript 需要沿着对象链找到变量或函数。
编辑:在这里可能不是这种情况。

实际上,任何合理的Java实现都需要零时间来“上升作用域链”。每个类都有一个方法表,超类的方法指针会被复制下来,因此只需要查询一个表。而Math是一个类,不是任何东西的“成员”。 - Hot Licks
嗯,我一直以为所有东西都是窗口对象的一部分。 - Jeffrey Sweeney
3
我们正在谈论的是JavaScript,而不是Java。JS没有类,它是一种基于原型的语言。 - GregL

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