В настоящее время я работаю над некоторым кодом, чувствительным к производительности, в Go. В какой-то момент у меня особенно плотная внутренняя петля, которая выполняет три операции подряд:Функции Variadic, вызывающие ненужные распределения кучи в 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
Однако, если удалить оператор печати, я получаю следующее:
$ go build alloc_no_print.go && time ./alloc_no_print
real 0m4.070s
user 0m4.063s
sys 0m0.008s
Поскольку 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()
с VARIADIC функции, которая ничего не делает и принимает *int
S в качестве параметров вместо interface{}
с:
func VarArgsError(ptrs ...*int) {
panic("An error has occurred.")
}
Я думаю, что это поведение, потому что Go выделяет указатели на куче всякий раз, когда они помещаются в срезе (хотя я не уверен, что это фактическое поведение процедур анализа эвакуации, я не вижу, как это безопасно было бы сделать иначе).
В этом вопросе есть две цели: во-первых, я хочу знать, правильный ли мой анализ ситуации, так как я не совсем понимаю, как работает анализ экранов Go. Во-вторых, мне нужны предложения по поддержанию поведения исходной программы, не вызывая ненужных распределений. Моя догадка, чтобы обернуть 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
}
}
В то время как это дает мне такую же производительность, как ни в print statement case, это странно, а не то, что я хочу переписать для каждого типа переменной, а затем обернуть вокруг все моего кода регистрации ошибок.
Ну, для начала пакет 'fmt' в значительной степени использует отражение, чтобы получить всю свою фантастическую структуру печати. Это может быть узким местом, если вы действительно стремитесь к производительности. Я понимаю, что его даже не называют, но об этом еще о чем подумать. Могу ли я спросить, что произойдет, если вы напишете свою собственную вариационную функцию, которая принимает аргументы, которые являются __NOT__ типа 'interface {}'? Вы видите те же проблемы? –
Да, я тестировал его на вариационной функции, которая принимала '* int' как аргументы, но забыла указать это или включить источник (который я сейчас сделал). Результаты те же, что и для 'Printf()'. Кроме того, по причинам, о которых вы упомянули, я обычно не использую пакет 'fmt' в разделах, которые имеют решающее значение для производительности. Хотя это, безусловно, хорошо, что нужно отметить. – mansfield
О, только немного красивее, но еще один вариант: 'ptr1, ptr2: = ptr1, ptr2' внутри блока if. Если компилятор не оптимизирует это, теперь две переменные, объявленные внутри «if», убегают, что может быть аналогично временному возврату из «Копия», созданного внутри escape-кода if. – twotwotwo