Skip to content

记录一个Go for-range循环的BUG

Posted on:2020-09-06 at 15:38

在 Go 1.13 和 1.14 版本现存一个很玄学的 BUG,在 for 循环中的一个常数会导致无法退出循环,一个简单的示例:

package main
func main() {
nums := []int64{1, 2}
for i, _ := range nums {
if i + 1 < 1 {
return
}
println(i)
}
}

很简单的一段代码,预期输出如下

0
1

但实际运行发现程序进入死循环,i 无限增长,继续测试发现,把 i+1 改成 i+2,运行就正常了!

这就很玄学了,循环体内的一个常数导致 for-range 越界了 😳

BUG 与编译器优化

当然这是一篇水文,这个问题也不是我发现的,在 7 月份就已经有人提交了 Issue,大佬们已经对这个 bug 做了解答,大概就是编译器在编译优化阶段,会进行一系列的代码分析,每 pass 一个会完成一个优化策略,其中一个 pass 不小心把越界判断给删掉了 😅

更详细一点,编译时生成的 IR(中间代码)中会有这么一行类似的代码:

v1 = nums.len
...
v2 = Less(i, v1)

先拿到切片长度 v1,然后判断下标 i 是否比 v1 小,再运行其他代码。问题就发生在这里,Less这一行在优化后消失了,消失原因是这一行是“死”代码。

大佬们发现,让Less变成 dead 的原因发生在prove pass优化过程中,而它的作用是优化不必要的越界判断。

Prove pass

prove pass优化是很重要的,因为原则上每次访问数组元素都会检查一次越界,但很多越界情况是可以在编译期发现的,这个 pass 可以优化掉不必要的越界判断。

比如,下面这段代码:

func (bigEndian) PutUint64(b []byte, v uint64) {
_ = b[7] // early bounds check to guarantee safety of writes below
b[0] = byte(v >> 56)
b[1] = byte(v >> 48)
b[2] = byte(v >> 40)
b[3] = byte(v >> 32)
b[4] = byte(v >> 24)
b[5] = byte(v >> 16)
b[6] = byte(v >> 8)
b[7] = byte(v)
}

函数第一行就访问下标为 7 的元素,它保证了这个数组的长度是大于等于 8 的,所以后面的代码都不必再进行越界判断。

在 BUG 代码中,i+1恰好满足了prove pass的某种优化条件,导致越界判断的Less代码变成了 dead 代码,进而被其他 pass 优化掉了。

哪个版本会修复这个 BUG?

在发本文的时候,Go 1.13 和 1.14 版本仍然在维护,相关的 patch 已经提交而且合入了(居然是同事大佬提交的 patch,震惊了),但是 1.15 版本的开发周期已经冻结,所以预计会在 1.16 的开发中合入。

一开始很奇怪,为什么这么严重的 bug,会因为“开发周期冻结”这么扯淡的理由延期修复,后来才了解到,Go 的开发周期 6 个月一次,前 3 个月开发迭代,后 3 个月冻结,在冻结期进行测试和优化,然后再通过发行小版本来合入 patch。

哎,突然很后悔没出海入个外企,这种事要是发生在国内,不让你加班修 bug 就不错了!