I have this piece of code, which is supposed to do no allocations at all, but for some reason it does. As benchmarks say, there happens 2 allocs/op.
Which lines of the functions do the allocation and why?
The functions:
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
}
The benchmark:
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()
}
}
I suppose the buf
slice somehow escapes, but I can't figure out how, because as far as I understand a slice is a struct allocated on the stack in this case, which would point to the variable b
as its data. I tried changing the expression unsafe.Slice(&b, 1)
to (*[1]byte)(unsafe.Pointer(&b))[:]
, but it changed nothing.
When a value is boxed in an interface, it's always considered to escape — even if the value is never used outside of the call stack, Go just stops the analysis at that point and considers that someone might have gotten hold of the address, and therefore the value has to go on the heap.
Since
Read
takesio.Reader
andWrite
takesio.Writer
,buf
(which is abytes.Buffer
passed to both functions) has to escape.Even if you made those functions take the concrete type
bytes.Buffer
(which you presumably don't want), that wouldn't be enough becauseRead
callsio.ReadFull
which again takes anio.Reader
. You'd have to work harder than that to make this allocation-free.As a side note, there's a much easier solution to your other issue in
Read
that doesn't require anyunsafe.Slice
shenanigans: just replacevar b byte
withvar b [1]byte
(which is exactly the same in memory), passb[:]
toReadFull
, and useb[0]
in the other places you useb
.