问题描述
在 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 对浮点数而不是整数进行运算。
为什么编译器不优化功耗操作来实现类似于位移的性能?
解决方法
首先,请注意 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格式,也必须转换为IEEE 754格式,因为{的返回值{1}} 是 math.Pow()
(尽管这些数字可以被缓存),您很可能再次将其转换回 float64
。
同样:如果您需要性能,请使用显式位移。
,因为这种优化从未实现过。
go 编译器的目标是拥有快速的编译时间。因此,一些优化被认为是不值得的。这以一些运行时间为代价节省了编译时间。