Performance 导致Go中不必要堆分配的可变函数

Performance 导致Go中不必要堆分配的可变函数,performance,memory-management,go,variadic-functions,escape-analysis,Performance,Memory Management,Go,Variadic Functions,Escape Analysis,我目前正在Go中编写一些性能敏感的代码。在某一点上,我有一个特别紧密的内部循环,它连续做三件事: 获取多个指向数据的指针。在发生罕见错误的情况下,这些指针中的一个或多个可能是nil 检查是否发生此错误,如果发生,则记录错误 使用指针中存储的数据 下面显示的是一个具有相同结构的玩具程序(尽管指针永远不可能是零) 但是,如果删除print语句,则会得到以下结果: $ go build alloc.go && time ./alloc real 0m5.466s user

我目前正在Go中编写一些性能敏感的代码。在某一点上,我有一个特别紧密的内部循环,它连续做三件事:

  • 获取多个指向数据的指针。在发生罕见错误的情况下,这些指针中的一个或多个可能是
    nil

  • 检查是否发生此错误,如果发生,则记录错误

  • 使用指针中存储的数据

  • 下面显示的是一个具有相同结构的玩具程序(尽管指针永远不可能是零)

    但是,如果删除print语句,则会得到以下结果:

    $ go build alloc.go && time ./alloc 
    
    real    0m5.466s
    user    0m5.458s
    sys     0m0.015s
    
    $ 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作为参数,而不是
    接口{}
    s:

    func VarArgsError(ptrs ...*int) {
        panic("An error has occurred.")
    }
    
    我认为这种行为是因为Go在将指针放在片中时会在堆上分配指针(虽然我不确定这是否是转义分析例程的实际行为,但我不知道它如何安全地执行其他操作)

    这个问题有两个目的:首先,我想知道我对形势的分析是否正确,因为我真的不明白Go’s escape分析是如何工作的。第二,我想得到一些建议,以便在不造成不必要的分配的情况下保持原始程序的行为。我的最佳猜测是在将指针传递到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
        }
    }
    
    虽然这给了我与无打印语句相同的性能,但这很奇怪,也不是我想为每种变量类型重写,然后将我所有的错误记录代码包装起来的那种情况。

    From

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

    当指针被传递给函数时,我认为它无法完成转义分析的第二部分。例如,函数可以将指针指定给其包中的全局变量,该变量的寿命比当前堆栈的寿命长。我认为当前的编译器不会进行如此深入的转义分析

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

    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
        }
    }
    
    func DoWork(){
    总和:=0
    n1,n2:=新(int),新(int)
    对于i:=0;i
    首先,
    fmt
    包大量利用反射来获得它所做的所有奇特的结构打印。如果你真的在追求性能,这可能是一个瓶颈。我意识到它甚至没有被叫来——但这是另一件值得思考的事情。我可以问一下,如果您编写自己的变量函数来接受非
    接口{}
    类型的参数,会发生什么情况?你看到同样的问题了吗?是的,我在一个变量函数上测试了它,该函数以
    *int
    s作为参数,但忘记了指定它或包含源(我现在已经做了)。结果与使用
    Printf()
    的结果相同。此外,出于您提到的原因,我通常不会在对性能至关重要的部分使用
    fmt
    包。虽然这当然是一件值得注意的事情。哦,只是稍微漂亮一点,但还有另一个选项:
    ptr1,ptr2:=ptr1,ptr2
    ,在if块内。除非编译器对此进行优化,否则现在它是在转义的“if”中声明的两个变量,这可能类似于从
    if
    转义中创建的
    Copy
    返回的临时变量。尽管如此,非常奇怪,有点不幸,可能值得发布到
    golang nuts
    。Re:“Go不能简单地证明Printf没有将输入隐藏在某个地方”:更准确地说:实际上只适用于函数参数和局部变量。由于
    Printf
    的形式参数是一个切片,因此转义分析只验证切片没有转义。它不需要检查切片的元素是否存在(实际的参数)可能会逃逸。这就是mansfield的假设,即“Go会在堆上分配指针,只要指针放在一个片中“。更准确地解释了原因。无论如何,如果调用的变量函数未更改值或存储指针以供以后使用,则解决方案有效,因此,如果OP只是在出现错误时执行Printf,则没有问题。请记住我所说的,您的代码强制复制传入数据(n1和n2)在循环的每次迭代中,这在本例中是次优的。他可能正在处理指向比int大得多的结构的指针。他的唯一解决方案是只在转义块中复制数据(
    如果ptr1==nil…{
    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
        }
    }