Go语言 为什么将'interface{}'转换回切片会导致额外的堆分配?

siv3szwd  于 2023-03-16  发布在  Go
关注(0)|答案(2)|浏览(75)
func BenchmarkPool(b *testing.B) {
    b.ReportAllocs()
    p := sync.Pool{New: func() interface{} {
        return make([]byte, 1024)
    }}
    for i := 0; i < b.N; i++ {
        bts := p.Get().([]byte)
        p.Put(bts)
    }
}

这个基准测试在go1.19.5中给出了以下输出。

BenchmarkPool
BenchmarkPool-10        47578498            24.47 ns/op       24 B/op          1 allocs/op

使用*[]byte时,情况会有所不同:
一个二个一个一个
似乎将interface{}转换回切片会导致额外的堆分配。
Go为什么需要这个额外的分配,其下的设计考虑是什么?

bwitn5fc

bwitn5fc1#

从您根据需要分配和丢弃标头和后备数组的意义上说,片是“动态的”。当您池化[]byte值时,总是复制片标头,因此实际上重用的是后备数组,并且每次都需要为标头分配额外的24个字节以将片返回到池中。换句话说,当你只传入一个拷贝时,你不能重用切片头。
如果希望池存储和重用整个切片值,则需要使用*[]byte,因为需要一个指针指向要存储在池中的值,这样还可以向池返回不同大小的切片值(可能通过append进行放大),这对于您的用例可能是理想的,也可能不是理想的。
如果您只想重用一个静态大小的缓冲区,那么您需要的是一个数组,或者更具体地说,是一个指向数组的指针,这样可以更清楚地了解池值的期望值。

func BenchmarkPool(b *testing.B) {
    b.ReportAllocs()
    p := sync.Pool{New: func() interface{} {
        return &[1024]byte{}
    }}
    for i := 0; i < b.N; i++ {
        buff := p.Get().(*[1024]byte)
        p.Put(buff)
    }
}

您可以根据需要在该数组周围分配一个切片,并在将其返回到池时将其转换回数组。

kx1ctssn

kx1ctssn2#

不是any[]byte的转换导致分配,而是[]byteany的转换导致分配。p.Put(bts)隐式地将参数bts转换为any,然后将其传递给(*sync.Pool).Put。GoGC 1.19中的接口被实现为一对指针,一个指向类型元数据,一个指向实际对象,在这种情况下,第二个指针逃逸到池,导致分配切片对象。这不仅适用于切片类型,也适用于任何其他非指针类型。
对于指针,如*[]byte,编译器会执行一个优化,将其值直接放入iface结构体,这样在转换为接口时会删除*[]byte示例的分配。因此,通常建议将指针放入池中,而不是结构体本身。

相关问题