Go含相同提前返回逻辑的数组右旋函数基准性能差异问题
Go数组右旋函数n=0时20倍性能差问题
问题背景
我实现了两个将输入数组 n 次向右旋转的函数。
两种实现均包含提前退出逻辑:当完成全部旋转后结果与原数组相等时,函数不执行任何操作直接返回,该场景出现在 n 为0或为数组长度整数倍的情况下。
第一个实现(拷贝临时数组方案):
func rotateRight1(nums []int, n int) { n = n % len(nums) if n == 0 { return } lastNDigits := make([]int, n) copy(lastNDigits, nums[len(nums)-n:]) copy(nums[n:], nums[:len(nums)-n]) copy(nums[:n], lastNDigits) }
第二个实现(原地环状替换方案):
func rotateRight2(nums []int, n int) { n = n % len(nums) if n == 0 { return } i := 0 current := nums[i] iAlreadySeen := i for j := 0; j < len(nums); j++ { nextI := (i + n) % len(nums) nums[nextI], current = current, nums[nextI] i = nextI // 处理数组长度为偶数时,步长可能提前回到已访问索引的问题 if nextI == iAlreadySeen { i = (i + 1) % len(nums) iAlreadySeen = i current = nums[i] } } }
基准测试代码如下:
func BenchmarkRotateRight1(b *testing.B) { nums := make([]int, 5_000) b.ResetTimer() b.ReportAllocs() for i := 0; i < b.N; i++ { rotateRight1(nums, 0) } } func BenchmarkRotateRight2(b *testing.B) { nums := make([]int, 5_000) b.ResetTimer() b.ReportAllocs() for i := 0; i < b.N; i++ { rotateRight2(nums, 0) } }
执行 go test -bench=. 命令在 go version go1.18 linux/amd64 环境下稳定得到如下测试结果:
cpu: Intel(R) Core(TM) i7-7500U CPU @ 2.70GHz BenchmarkRotateRight1-4 1000000000 0.4603 ns/op 0 B/op 0 allocs/op BenchmarkRotateRight2-4 97236492 12.11 ns/op 0 B/op 0 allocs/op PASS
两个函数都会在 n == 0 时触发提前返回,逻辑看起来完全一致,却出现了20倍以上的性能差距。
原因说明
性能差异完全来自Go 1.18编译器的优化粒度区别,和代码逻辑正确性无关:
- 对于
rotateRight1,n==0分支之后的所有代码(切片分配、内存拷贝)都依赖n>0的前提才会执行,编译器可以明确判断出n=0时整个函数没有任何内存访问、没有任何副作用,因此直接把基准测试循环里的函数调用完全优化掉了。测出来的0.46ns/op本质是空循环的耗时,根本没有实际执行函数调用逻辑。 - 对于
rotateRight2,if n==0 {return}的代码后面紧跟着current := nums[i]这类固定访问切片下标0的操作。哪怕n=0时这些代码永远不会运行,Go 1.18的编译器做不到跨分支做这么深的无效代码消除,也没法把这个函数完全内联优化为空操作,每次调用都需要执行完整的函数栈准备、取切片长度、取模、条件判断流程,耗时自然就落到了十几纳秒的水平。
验证方式很简单:把rotateRight2里if n==0 {return}之后的所有代码全部注释掉,再跑基准测试,两个函数的性能就会完全一致。
内容的提问来源于stack exchange,提问作者Rafael FT
相关产品推荐
相关产品推荐

