Go语言VarInt读写函数意外堆分配问题排查求助
VarInt读写函数的堆分配问题分析
问题背景
我编写了一段理论上不应产生内存分配的Go代码,但基准测试显示每次操作存在2次堆分配。需要找出读写函数中导致分配的代码行及原因。
相关函数代码
func (vi *VarInt /* int32 */) Read(input io.Reader) error { var ( b byte buf = unsafe.Slice(&b, 1) shift int value uint32 ) for { _, err := io.ReadFull(input, buf) if err != nil { return err } value |= (uint32(b) & 0b01111111) << shift if (b & 0b10000000) == 0 { *vi = VarInt(value) return nil } shift += 7 if shift >= 32 { return ErrVarIntTooLong } } } func (vi *VarInt /* int32 */) Write(output io.Writer) error { var ( varint [5]byte uvalue = uint32(*vi) x int ) for ; ; x++ { vb := uint8(uvalue) if (vb & 0b10000000) == 0 { varint[x] = vb break } varint[x] = (vb & 0b01111111) | 0b10000000 uvalue >>= 7 } _, err := output.Write(varint[:x+1]) if err != nil { return err } return nil }
基准测试代码
func BenchmarkVarInt(b *testing.B) { var buf bytes.Buffer buf.Grow(5) b.ResetTimer() for i := 0; i < b.N; i++ { vi := (VarInt)(i) vi.Write(&buf) vi.Read(&buf) buf.Reset() } }
问题分析
两次堆分配分别来自Read和Write函数,核心原因是栈上切片/数组因接口传递触发逃逸分析,被分配到堆上:
Read函数中的
buf切片- 你用
unsafe.Slice(&b, 1)创建的切片,底层数组是栈上的byte变量b。但当你将这个切片传入io.ReadFull(input, buf)时,input是io.Reader接口类型。 - Go的逃逸分析器无法确定接口方法(这里是
bytes.Buffer.Read)是否会持有切片的引用,为了保证内存安全,会将b和对应的切片结构体判定为需要逃逸到堆,从而产生一次分配。
- 你用
Write函数中的
varint[:x+1]切片varint是栈上的[5]byte数组,你取它的切片varint[:x+1]传入output.Write,而output是io.Writer接口类型。- 同样的逻辑,逃逸分析器无法排除接口方法持有切片引用的可能性,因此将栈上的
varint数组判定为需要逃逸到堆,产生第二次分配。
你尝试将unsafe.Slice(&b, 1)改为(*[1]byte)(unsafe.Pointer(&b))[:]没有效果,是因为两种方式创建的切片本质都是指向栈上变量的切片,都会触发同样的逃逸逻辑。
可以通过执行go build -gcflags="-m=2"查看逃逸分析的详细输出,会明确看到b escapes to heap和varint escapes to heap的提示。
内容的提问来源于stack exchange,提问作者kerelape
相关产品推荐
相关产品推荐

