奇怪的JS性能问题

5

我遇到了字符串距离函数运行过慢的问题。我已经缩小了范围,下面是比较耗时的部分(时间是10K个字符串比较的时间):

// desired behavior - 400ms
dp[c + 257] = Math.min(dp[c + 256] + 1, dp[c + 1] + 1, dp[c] + (a[i] != b[j]));

// this is somewhat faster - 300ms
dp[c + 257] = Math.min(dp[c + 256] + 1, dp[c + 1] + 1);
dp[c + 257] = Math.min(dp[c + 257], dp[c] + (a[i] != b[j]));

// even faster - 50ms
dp[c + 257] = Math.min(dp[c + 256] + 1, dp[c + 1] + 1);
dp[c + 257] = Math.min(dp[c + 257], dp[c] + (a[i] != b[j] ? 1 : 0));

首先,将 Math.min 拆分成两个调用比一次使用三个参数更快 - 这是如何可能的?其次,为什么添加一个显式的三元表达式比依赖于从布尔值到整数的隐式转换要快得多?
这里有一个小示例:https://jsfiddle.net/6bnLvbt6/

1
类型转换比不进行类型转换更耗费资源并不让我感到惊讶。类型转换是需要代价的。 - Felix Kling
我建议你创建一个jsperf而不是一个jsfiddle。 - Alberto Zaccagni
但是 a[i]!=b[j]?1:0 也基本上是类型转换,只不过它在JS中是显式完成的,肯定比本地的更昂贵? - riv
在V8中,Math.min的二元版本与具有任意参数的Math.min完全不同的代码路径。 - apsillers
哦,Math.min 实际上是在 JS 中实现的,而不是原生代码中吗? - riv
1个回答

2

您有3个测试用例:

  1. Math.min 使用类型转换(bool to int)的3个参数
  2. 使用2个参数的2Math.min调用,其中第二个调用具有类型转换
  3. 使用2个参数的2Math.min调用,不需要类型转换

1和2&3之间还有另一个区别,即某些参数上有额外的+ 1。我认为这是一个错误。

隐式类型转换似乎是最昂贵的。如果从1中删除隐式类型转换,则会得到:

dp[c + 257] = Math.min(dp[c + 1], dp[c + 256], dp[c] + (a[i] != b[j] ? 1 : 0));

这需要 250ms 的时间(相对于3的 60ms)。这仍然表明使用3个参数的Math.min速度较慢。让我们进一步调查。
如果您将测试用例简化为:
// 1
for(var i = 0; i < 10000; i += 1) {
    Math.min(1, 2, 3);
}
// 2
for(var i = 0; i < 10000; i += 1) {
    Math.min(1, 2);
    Math.min(1, 3);
}

我的机器上的结果是:1花费2500ms2花费87ms。如果您向Math.min添加更多参数,您会发现它每增加一个额外参数就会以500ms的速度递增,这揭示了一些关于实现的信息。
浏览器厂商优化经常使用的函数。如果带有超过2个参数的Math.min不常见,则这些结果并不奇怪。你在这里看到的是Math.min有两个实现:一个用于两个参数,它被真正地优化,另一个用于更多参数,它没有被优化。对于v8,apsillers证实了这一点:v8 source on github

问题更多地是“为什么Math.min的实现方式手动展开会比原生函数快得多,当它应该是一个本地函数(否则我只需编写a<b?a:b)”,以及“为什么从bool到int的隐式类型转换比x?1:0慢,当它做相同的事情”。 - riv
优化就是要充分利用你所拥有的所有信息。如果你的信息是“我有超过2个数字”,那么你知道的比“我恰好有3个数字”时少。Math.min可以通过编写比较3个数字的代码(一堆if语句)来实现快速处理3个数字。为什么没有编写这样的代码呢?可能是因为没有人认为它很重要。如果你经常比较3个数字,那么可以自己编写一个math_min_3函数。 - Halcyon
为什么隐式布尔转数字会变慢?因为你需要添加信息来使其更快。如果你事先知道你有一个布尔值,你就不需要昂贵的发现变量类型的过程。 - Halcyon
我的猜测是a<b?a:b比使用Math.min更快。为什么呢?首先,因为没有函数开销;其次,因为你知道得更多。Math.min必须处理任何参数都可能是NaN的情况。如果你知道(或者不关心)你的任何参数是否为NaN,那么你可以利用这一点来编写更快的实现。 - Halcyon

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