变量函数在Go中导致不必要的堆分配

时间:2015-01-05 22:14:10

标签: performance memory-management go variadic-functions escape-analysis

我目前正在研究Go中的一些性能敏感代码。有一点,我有一个特别严密的内环,连续做三件事:

  1. 获取数据指针。如果发生罕见错误,这些指针中的一个或多个可能是nil

  2. 检查是否发生了此错误,并记录错误。

  3. 使用存储在指针中的数据。

  4. 下面显示的是一个具有相同结构的玩具程序(虽然指针实际上永远不会是零)。

    package main
    
    import (
        "math/rand"
        "fmt"
    )
    
    const BigScaryNumber = 1<<25
    
    func DoWork() {
        sum := 0
        for i := 0; i < BigScaryNumber; i++ {
            // Generate pointers.
            n1, n2 := rand.Intn(20), rand.Intn(20)
            ptr1, ptr2 := &n1, &n2
    
            // Check if pointers are nil.
            if ptr1 == nil || ptr2 == nil {
                fmt.Printf("Pointers %v %v contain a nil.\n", ptr1, ptr2)
                break
            }
    
            // Do work with pointer contents.
            sum += *ptr1 + *ptr2
        }
    }
    
    func main() {
        DoWork()
    }
    

    当我在我的机器上运行时,我得到以下内容:

    $ go build alloc.go && time ./alloc 
    
    real    0m5.466s
    user    0m5.458s
    sys     0m0.015s
    

    但是,如果我删除print语句,我会得到以下内容:

    $ go build alloc_no_print.go && time ./alloc_no_print
    
    real    0m4.070s
    user    0m4.063s
    sys     0m0.008s
    

    由于从未实际调用print语句,因此我调查了print语句是否以某种方式导致指针在堆而不是堆栈上分配。在原始程序上使用-m标志运行编译器会给出:

    $ go build -gcflags=-m alloc.go
    # command-line-arguments
    ./alloc.go:14: moved to heap: n1
    ./alloc.go:15: &n1 escapes to heap
    ./alloc.go:14: moved to heap: n2
    ./alloc.go:15: &n2 escapes to heap
    ./alloc.go:19: DoWork ... argument does not escape
    

    在打印无语句程序中执行此操作时

    $ go build -gcflags=-m alloc_no_print.go
    # command-line-arguments
    ./alloc_no_print.go:14: DoWork &n1 does not escape
    ./alloc_no_print.go:14: DoWork &n2 does not escape
    

    确认即使是未使用的fmt.Printf()也会导致堆分配对性能产生非常实际的影响。我可以通过将fmt.Printf()替换为无变量函数来实现相同的行为,该函数不执行任何操作并将*int s作为参数而不是interface{} s:

    func VarArgsError(ptrs ...*int) {
        panic("An error has occurred.")
    }
    

    我认为这种行为是因为Go在堆放入切片时会在堆上分配指针(尽管我不确定这是转义分析例程的实际行为,但我看不出它是如何安全的能够做到其他)。

    这个问题有两个目的:首先,我想知道我对情况的分析是否正确,因为我真的不明白Go的逃逸分析是如何运作的。其次,我想要保持原始程序行为的建议,而不会导致不必要的分配。我最好的猜测是在将指针传递给print语句之前在指针周围包装一个Copy()函数:

    fmt.Printf("Pointers %v %v contain a nil.", Copy(ptr1), Copy(ptr2))
    

    其中Copy()定义为

    func Copy(ptr *int) *int {
        if ptr == nil {
            return nil
        } else {
            n := *ptr
            return &n
        }
    }
    

    虽然这给了我与no print语句相同的性能,但它很奇怪,而不是我想要为每个变量类型重写然后包装所有我的错误记录代码

1 个答案:

答案 0 :(得分:1)

来自Go FAQ

  

在当前的编译器中,如果变量的地址被采用,那么   变量是堆上分配的候选者。但是,基本的   转义分析可以识别某些情况,而这些变量则不会   过了函数的返回,可以驻留在堆栈上。

当指针传递给函数时,我认为它失败了逃逸分析的第二部分。例如,该函数可以将指针分配给其包中的全局变量,该变量的寿命比当前堆栈长。我不认为当前的编译器会进行这种深度逃逸分析。

避免分配成本的一种方法是将分配移到循环之外,并将值重新分配给循环内的已分配内存。

func DoWork() {
    sum := 0
    n1, n2 := new(int), new(int)

    for i := 0; i < BigScaryNumber; i++ {
        *n1, *n2 = rand.Intn(20), rand.Intn(20)
        ptr1, ptr2 := n1, n2

        // Check if pointers are nil.
        if ptr1 == nil || ptr2 == nil {
            fmt.Printf("Pointers %v %v contain a nil.\n", n1, n2)
            break
        }

        // Do work with pointer contents.
        sum += *ptr1 + *ptr2
    }
}