指针切片中的golang奇怪分配

0h4hbjxa  于 2024-01-04  发布在  Go
关注(0)|答案(1)|浏览(106)

我有一个简单的基准来比较创建结构片和指向该结构片的指针的性能

package pointer

import (
    "testing"
)

type smallStruct struct {
    ID int
}

func newSmallStruct(id int) *smallStruct {
    return &smallStruct{ID: id}
}

func BenchmarkSmallStructPointer(b *testing.B) {
    for n := 0; n < b.N; n++ {
        var slice = make([]*smallStruct, 0, 10000)
        for i := 0; i < 10000; i++ {
            t := newSmallStruct(n + i)
            slice = append(slice, t)
        }
    }
}

func BenchmarkSmallStruct(b *testing.B) {
    for n := 0; n < b.N; n++ {
        var slice = make([]smallStruct, 0, 10000)
        for i := 0; i < 10000; i++ {
            t := newSmallStruct(n + i)
            slice = append(slice, *t)
        }
    }
}

字符串
基准结果

go test -bench . -benchmem
goos: linux
goarch: amd64
pkg: test-project/pointer
cpu: Intel(R) Core(TM) i5-6200U CPU @ 2.30GHz
BenchmarkSmallStructPointer-4               3121            328864 ns/op          161921 B/op      10001 allocs/op
BenchmarkSmallStruct-4                     29218             48021 ns/op           81920 B/op          1 allocs/op


请解释一下,是什么操作产生了这么多的指针切片分配?它似乎是追加到切片,但我不明白为什么?

q5iwbnjs

q5iwbnjs1#

Go语言有一个名为“逃逸分析”的特性,它可以记录哪些值逃逸到堆中,以及为什么逃逸。
它通过一个可选详细程度的编译器标志启用:-gcflags='-m=2'。您可以在go testgo buildgo run上设置它。(任何将该标志沿着传递给go tool compile的内容)
使用这个我们可以看到&smallStruct逃逸到堆,强制分配。如果我正确解释结果的话,这是因为即使newSmallStruct在两个测试用例中都是内联的,编译器也可以看到指针在第二种情况下立即被解引用。而第一种情况继续将指针传递给其他函数(append):

> go test -bench=. -benchmem -gcflags='-m=2' .
# ...

./main_test.go:12:9: &smallStruct{...} escapes to heap:
./main_test.go:12:9:   flow: ~r0 = &{storage for &smallStruct{...}}:
./main_test.go:12:9:     from &smallStruct{...} (spill) at ./main_test.go:12:9
./main_test.go:12:9:     from return &smallStruct{...} (return) at ./main_test.go:12:2

# ...
./main_test.go:29:23: &smallStruct{...} does not escape

字符串
具有更高的详细度(3):

./main_test.go:19:23: &smallStruct{...} escapes to heap:
./main_test.go:19:23:   flow: ~R0 = &{storage for &smallStruct{...}}:
./main_test.go:19:23:     from &smallStruct{...} (spill) at ./main_test.go:19:23
./main_test.go:19:23:     from ~R0 = &smallStruct{...} (assign-pair) at ./main_test.go:19:23
./main_test.go:19:23:   flow: t = ~R0:
./main_test.go:19:23:     from t := ~R0 (assign) at ./main_test.go:19:6
./main_test.go:19:23:   flow: {heap} = t:
./main_test.go:19:23:     from append(slice, t) (call parameter) at    <--
./main_test.go:20:18


本演讲详细介绍了各种情况:https://www.youtube.com/watch?v=ZMZpH4yT7M0

围绕此优化

即使没有append-使用普通索引分配-这也会导致堆分配。
这是因为如果&smallStruct{...}是在newSmallStruct()的堆栈框架中分配的,那么该地址可能会在以后被覆盖。Go会重新使用旧的函数堆栈空间来创建新的函数。在这里,在堆中分配是要做的“可验证正确”的事情。
如果你有一个情况下,你需要优化性能,首先创建一个只值的切片-理想情况下返回一个来自newSmallStruct()的值,这样它就可以在不内联的情况下工作。然后让指针切片引用这些本地值。

func BenchmarkSmallStructPointer(b *testing.B) {
    for n := 0; n < b.N; n++ {
        vslice := make([]smallStruct, 0, 10000)
        for i := 0; i < 10000; i++ {
            t := newSmallStruct(n + i)
            vslice = append(vslice, *t)
        }

        slice := make([]*smallStruct, len(vslice))
        for i := 0; i < len(vslice); i++ {
            slice[i] = &vslice[i]
        }
    }
}
BenchmarkSmallStructPointer-8              45446             25011 ns/op          163840 B/op          2 allocs/op

的字符串

相关问题