为什么 math.Pow 性能比位移差?

Why is math.Pow performance worse than bitshifting?

在Exercism网站上求解this exercise时,我使用了标准的math.Pow包函数来求2的乘方。

return uint64(math.Pow(2, float64(n-1)))

查看社区解决方案后,我找到了一个使用位移来实现相同目的的解决方案:

return uint64(1 << uint(n-1)), nil

令我吃惊的是,两者之间的性能差异很大: bit-shifting math-pow

我认为 Go 编译器会识别出 math.Pow 使用常量 2 作为基数,并且只使用它自己的位移位,而无需我明确地这样做。我能看到的唯一其他区别是 float64 的转换,并且 math.Pow 对浮点数而不是整数进行操作。

为什么编译器不优化幂运算来达到类似于位移位的性能?

因为从未实施过此类优化。

go 编译器的目标是具有快速的编译时间。因此,一些优化被认为是不值得的。这节省了一些 运行 时间的编译时间。

首先,请注意 uint64(1) << (n-1) 是您问题中出现的表达式 uint64(1 << uint(n-1)) 的更好版本。表达式 1<<n 是一个 int,因此有效的移位值介于 0 和 30 或 62 之间,具体取决于 int 的大小。 uint64(1) << n 允许 n 在 0 到 63 之间。

总的来说,您建议的优化是不正确的。编译器必须能够推断出 n 在特定范围内。

看这个例子(on playground)

package main

import (
    "fmt"
    "math"
)

func main() {
    n := 65
    fmt.Println(uint64(math.Pow(2, float64(n-1))))
    fmt.Println(uint64(1) << uint(n-1))
}

输出表明两种方法不同:

9223372036854775808
0

math.Pow() 实现为对 float64 数字进行操作。计算 2 的幂的位移位只能应用于整数,并且只能应用于结果符合 int64(或 uint64)的微小子集。

如果你有这样的特殊情况,非常欢迎你使用位移。

结果大于math.MaxInt64或基数不是2(或2的幂)的任何其他情况需要浮点运算。

另请注意,即使实现对上述可能的微小子集的检测,结果也是 2's complement format, which also would have to be converted to IEEE 754 格式,因为 math.Pow() 的 return 值为 float64(虽然这些数字可以被缓存),你很可能再次将其转换回 int64.

再次声明:如果您需要性能,请使用显式位移。