gpt4 book ai didi

performance - 可变参数函数在 Go 中导致不必要的堆分配

转载 作者:IT王子 更新时间:2023-10-29 01:06:15 26 4
gpt4 key购买 nike

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

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

  2. 检查是否发生此错误,如果发生则记录错误。

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

下面显示的是一个具有相同结构的玩具程序(尽管指针实际上永远不会为 nil)。

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

但是,如果我删除打印语句,我会得到以下信息:

$ 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 作为参数而不是 interface{} 的可变参数函数来获得相同的行为s:

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

我认为这种行为是因为 Go 会在将指针放入 slice 时在堆上分配指针(虽然我不确定这是逃逸分析例程的实际行为,但我不明白它如何安全能够做其他事情)。

这个问题有两个目的:第一,我想知道我对情况的分析是否正确,因为我不太了解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
}
}

虽然这给了我与无打印语句情况相同的性能,但它很奇怪而且不是我想为每种变量类型重写然后环绕我的错误记录代码的所有 .

最佳答案

来自 Go FAQ ,

In the current compilers, if a variable has its address taken, that variable is a candidate for allocation on the heap. However, a basic escape analysis recognizes some cases when such variables will not live past the return from the function and can reside on the stack.

当指针被传递给一个函数时,我认为它没有通过逃逸分析的第二部分。例如,该函数可以将指针分配给其包中的全局变量,该变量的生命周期比当前堆栈长。我认为当前的编译器不会进行如此深入的逃逸分析。

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

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
}
}

关于performance - 可变参数函数在 Go 中导致不必要的堆分配,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/27788813/

26 4 0
Copyright 2021 - 2024 cfsdn All Rights Reserved 蜀ICP备2022000587号
广告合作:1813099741@qq.com 6ren.com