英文:
Why does converting `interface{}` back to slice cause an additional heap allocation?
问题
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
时,情况就不同了:
func BenchmarkPool(b *testing.B) {
b.ReportAllocs()
p := sync.Pool{New: func() interface{} {
bts := make([]byte, 1024)
return &bts
}}
for i := 0; i < b.N; i++ {
bts := p.Get().(*[]byte)
p.Put(bts)
}
}
BenchmarkPool
BenchmarkPool-10 142008002 8.581 ns/op 0 B/op 0 allocs/op
似乎将 interface{}
转换回切片会导致额外的堆分配。
为什么 Go 需要这个额外的分配?在其中的设计考虑是什么?
英文:
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)
}
}
This benchmark gives the following output in go1.19.5.
BenchmarkPool
BenchmarkPool-10 47578498 24.47 ns/op 24 B/op 1 allocs/op
When use *[]byte
, things get different:
func BenchmarkPool(b *testing.B) {
b.ReportAllocs()
p := sync.Pool{New: func() interface{} {
bts := make([]byte, 1024)
return &bts
}}
for i := 0; i < b.N; i++ {
bts := p.Get().(*[]byte)
p.Put(bts)
}
}
BenchmarkPool
BenchmarkPool-10 142008002 8.581 ns/op 0 B/op 0 allocs/op
It seems that convert interface{}
back to slice cause an additional heap allocation.
Why does Go need this additional allocation? What's the design consideration under it?
答案1
得分: 1
一个切片在"动态"意义上是指根据需要分配和丢弃头部和后备数组。当你将[]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)
}
}
你可以根据需要分配一个围绕该数组的切片,并在将其返回到池中时将其转换回数组。
英文:
A slice is meant to be "dynamic" in the sense that you allocate and discard the header and backing array as needed. When you pool the []byte
value, the slice header is always copied, so all that is essentially reused is the backing array, and you need to allocate the extra 24 bytes for the header every time to return the slice to the pool. In other words, you can't reuse the slice header when you are only passing in a copy.
If you want the pool to store and reuse an entire slice value, then you want to use *[]byte
because you need a pointer to the value you want to store in the pool. This would also allow you to return a differently sized slice value to the pool (possibly enlarged with append), which may or may not be desirable for your use case.
If you want only a statically sized buffer for reuse, then what you want is an array, or more specifically, a pointer to an array, which makes it much clearer what the expectations are of the pooled value.
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)
}
}
You can allocate a slice around that array as needed, and convert it back to an array when returning it to the pool.
答案2
得分: 0
不是将any
转换为[]byte
导致了分配,而是将[]byte
转换为any
导致了分配。在将bts
传递给(*sync.Pool).Put
之前,p.Put(bts)
会隐式地将参数bts
转换为any
。在GoGC 1.19中,接口被实现为一对指针,一个指向类型元数据,一个指向实际对象。在这种情况下,第二个指针逃逸到池中,导致分配了切片对象。这不仅适用于切片类型,还适用于任何其他非指针类型。
对于指针类型,比如*[]byte
,编译器会执行一种优化,直接将其值放入iface
结构中,这样在转换为接口时就不会分配*[]byte
实例。因此,通常建议将指针放入池中,而不是结构本身。
英文:
It is not the conversion of any
to []byte
that causes allocation, it is the conversion of []byte
to any
that does. p.Put(bts)
implicitly casts the argument bts
to any
before passing it to (*sync.Pool).Put
. An interface as in GoGC 1.19 is implemented as a pair of pointers, one to the type metadata and one to the actual object, and it is the second pointer that escapes to the pool in this case that results in the slice object being allocated. This does not only hold for slice types but also any other non-pointer types.
For a pointer, such as a *[]byte
, the compiler performs an optimisation that put its value directly into the iface
struct, which removes the allocation of a *[]byte
instance when converting to an interface. As a result, it is often recommended to put the pointer in pools instead of the struct themselves.
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论