man*_*eld 9 performance memory-management variadic-functions go escape-analysis
我目前正在研究Go中的一些性能敏感代码.有一次,我有一个特别严密的内环,连续做三件事:
获取数据的几个指针.如果发生罕见错误,可能会有一个或多个指针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()
}
Run Code Online (Sandbox Code Playgroud)
当我在我的机器上运行时,我得到以下内容:
$ go build alloc.go && time ./alloc
real 0m5.466s
user 0m5.458s
sys 0m0.015s
Run Code Online (Sandbox Code Playgroud)
但是,如果我删除print语句,我会得到以下内容:
$ go build alloc_no_print.go && time ./alloc_no_print
real 0m4.070s
user 0m4.063s
sys 0m0.008s
Run Code Online (Sandbox Code Playgroud)
由于从未实际调用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
Run Code Online (Sandbox Code Playgroud)
在打印无语句程序中执行此操作时
$ 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
Run Code Online (Sandbox Code Playgroud)
确认即使是未使用的fmt.Printf()
也会导致堆分配对性能产生非常实际的影响.我可以通过替换fmt.Printf()
一个什么都不做的变量函数来获得相同的行为,并将*int
s作为参数而不是interface{}
s:
func VarArgsError(ptrs ...*int) {
panic("An error has occurred.")
}
Run Code Online (Sandbox Code Playgroud)
我认为这种行为是因为Go在堆放入切片时会在堆上分配指针(虽然我不确定这是转义分析例程的实际行为,但我看不出它是如何安全地能够否则).
这个问题有两个目的:首先,我想知道我对情况的分析是否正确,因为我真的不明白Go的逃逸分析是如何运作的.其次,我想要保持原始程序行为的建议,而不会导致不必要的分配.我最好的猜测是Copy()
在将指针传递给print语句之前在指针周围包装一个函数:
fmt.Printf("Pointers %v %v contain a nil.", Copy(ptr1), Copy(ptr2))
Run Code Online (Sandbox Code Playgroud)
在哪里Copy()
定义为
func Copy(ptr *int) *int {
if ptr == nil {
return nil
} else {
n := *ptr
return &n
}
}
Run Code Online (Sandbox Code Playgroud)
虽然这给了我与no print语句相同的性能,但它很奇怪,而不是我想为每个变量类型重写然后包装所有错误记录代码.
来自Go常见问题解答,
在当前的编译器中,如果一个变量的地址被获取,那么该变量就是在堆上分配的候选者。然而,基本的逃逸分析可以识别某些情况,即此类变量不会在函数返回之后继续存在,而是可以驻留在堆栈上。
当指针传递给函数时,我认为它无法完成转义分析的第二部分。例如,函数可以将指针分配给其包中的全局变量,该变量的寿命比当前堆栈的寿命长。我不认为当前的编译器会进行如此深度的转义分析。
避免分配成本的一种方法是将分配移到循环之外,并将值重新分配给循环内分配的内存。
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
}
}
Run Code Online (Sandbox Code Playgroud)