为什么 slice 不断地从堆栈中逃逸?

Why slice kept escaping from stack?

我正在尝试解决 leetcode 问题 permutations。 但是当我用 -benchmem 测试时,我发现它分配太多,当 permute([]int{1,2,3,4,5,6})

时达到 1957 allocs/op

我发现它在生成 sub-nums 目标时逃逸到堆中。即使我尝试分配 [6]int,并使用不安全的包来构建切片,它仍然 moved to heap.

我的问题是,为什么切片会逃逸到堆中,我如何在堆栈上分配切片?

这是我的代码:

package main

import (
    "fmt"
    "reflect"
    "unsafe"
)


func permute(nums []int) [][]int {
    resLen := 1
    for i := 1; i<= len(nums);i ++{
        resLen *= i
    }
    // pre allocate
    res := make([][]int, resLen)
    for i := range res{
        res[i] = make([]int, 0, len(nums))
    }

    build(res, nums)

    return res
}

func build(res [][]int,targets []int){
    step := len(res) / len(targets)
    for i := range targets{
        for j := i*step; j < (i+1) * step; j ++{
            res[j] = append(res[j], targets[i])
        }
        if len(targets) != 1{
            var ab = [6]int{}
            var buff []int
            var bp  *reflect.SliceHeader
            bp = (*reflect.SliceHeader)(unsafe.Pointer(&buff))
            bp.Data = uintptr(unsafe.Pointer(&ab))
            bp.Cap = 6
            buff = append(buff, targets[:i]...)
            buff = append(buff, targets[i+1:]...)
            build(res[i*step:(i+1)*step], buff)
        }
    }
    return
}

func main() {
    nums := []int{1,2,3}
    res := permute(nums)
    fmt.Println(res)
}

build 没有不安全但逃逸到堆的函数:

func build(res [][]int, targets []int) {
    step := len(res) / len(targets)
    for i := range targets {
        for j := i * step; j < (i+1)*step; j++ {
            res[j] = append(res[j], targets[i])
        }
        if len(targets) != 1 {
            buff := make([]int, 0, 6) //  make([]int, 0, 6) escapes to heap
            buff = append(buff, targets[:i]...)
            buff = append(buff, targets[i+1:]...)
            build(res[i*step:(i+1)*step], buff)
        }
    }
    return
}

我的测试用例:

package main

import "testing"

func Benchmark(b *testing.B){
    for i:=0;i<b.N;i++{
        permute([]int{1,2,3,4,5,6})
    }
}

当我运行go build -gcflags="-m"时,报告./main.go:32:8: moved to heap: ab

尝试使用 unsafe.Pointer 破坏编译器只会使逃逸分析更难完成其工作,从而阻止切片被堆栈分配。只需分配一个切片并在每次循环迭代中重复使用它:

func build(res [][]int, targets []int) {
    buff := make([]int, 0, 6)
    step := len(res) / len(targets)
    for i := range targets {
        buff = buff[:0]
        for j := i * step; j < (i+1)*step; j++ {
            res[j] = append(res[j], targets[i])
        }
        if len(targets) != 1 {
            buff = append(buff, targets[:i]...)
            buff = append(buff, targets[i+1:]...)
            build(res[i*step:(i+1)*step], buff)
        }
    }
    return
}

这可以被编译器正确优化

./main.go:26:17: make([]int, 0, 6) does not escape

并且只会产生所需的分配:

Benchmark-8  44607  26838 ns/op  52992 B/op  721 allocs/op