Функции Variadic, вызывающие ненужные выделения кучи в Go
В настоящее время я работаю над чувствительным к производительности кодом в 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()
}
Когда я запускаю это на своей машине, я получаю следующее:
$ 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 выделяет указатели в куче всякий раз, когда они помещаются в срез (хотя я не уверен, что это реальное поведение подпрограмм escape-анализа, я не понимаю, как он мог бы безопасно делай иначе).
Этот вопрос преследует две цели: во-первых, я хочу знать, верен ли мой анализ ситуации, поскольку я не совсем понимаю, как работает анализ побега Го. И во-вторых, я хотел предложения по поддержанию поведения исходной программы, не вызывая ненужных выделений. Мое лучшее предположение, чтобы обернуть Copy()
Обрабатывайте указатели перед передачей их в оператор print:
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 ответ
От Go FAQ,
В текущих компиляторах, если переменная имеет свой адрес, эта переменная является кандидатом для размещения в куче. Однако базовый анализ escape распознает некоторые случаи, когда такие переменные не будут жить после возврата из функции и могут находиться в стеке.
Когда указатели передаются в функцию, я думаю, что она не проходит вторую часть анализа выхода. Например, функция может назначить указатель на глобальную переменную в своем пакете, которая живет дольше, чем текущий стек. Я не думаю, что текущий компилятор делает такой глубокий анализ побега.
Один из способов избежать затрат на выделение - переместить выделение за пределы цикла и переназначить значение выделенной памяти внутри цикла.
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
}
}