7

В настоящее время я работаю над некоторым кодом, чувствительным к производительности, в Go. В какой-то момент у меня особенно плотная внутренняя петля, которая выполняет три операции подряд:Функции Variadic, вызывающие ненужные распределения кучи в Go

  1. Получить несколько указателей на данные. В случае редкой ошибки один или несколько из этих указателей могут быть nil.

  2. Проверьте, произошла ли эта ошибка, и зарегистрируйте ошибку, если она есть.

  3. Выполняйте работу с данными, хранящимися в указателях.

Ниже представлена ​​игрушечная программа с той же структурой (хотя указатели никогда не могут быть ничтожными).

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, это странно, а не то, что я хочу переписать для каждого типа переменной, а затем обернуть вокруг все моего кода регистрации ошибок.

+0

Ну, для начала пакет 'fmt' в значительной степени использует отражение, чтобы получить всю свою фантастическую структуру печати. Это может быть узким местом, если вы действительно стремитесь к производительности. Я понимаю, что его даже не называют, но об этом еще о чем подумать. Могу ли я спросить, что произойдет, если вы напишете свою собственную вариационную функцию, которая принимает аргументы, которые являются __NOT__ типа 'interface {}'? Вы видите те же проблемы? –

+0

Да, я тестировал его на вариационной функции, которая принимала '* int' как аргументы, но забыла указать это или включить источник (который я сейчас сделал). Результаты те же, что и для 'Printf()'. Кроме того, по причинам, о которых вы упомянули, я обычно не использую пакет 'fmt' в разделах, которые имеют решающее значение для производительности. Хотя это, безусловно, хорошо, что нужно отметить. – mansfield

+0

О, только немного красивее, но еще один вариант: 'ptr1, ptr2: = ptr1, ptr2' внутри блока if. Если компилятор не оптимизирует это, теперь две переменные, объявленные внутри «if», убегают, что может быть аналогично временному возврату из «Копия», созданного внутри escape-кода if. – twotwotwo

ответ

1

От Go FAQ,

В современных компиляторов, если переменная имеет свой адрес взятый, что переменная является кандидатом для размещения в куче. Однако базовый анализ удаления распознает некоторые случаи, когда такие переменные не будут жить за возвратом от функции и могут находиться в стеке.

Когда указатели передаются на функцию, я думаю, что она не выполняет вторую часть анализа эвакуации. Например, функция может назначить указатель на глобальную переменную в своем пакете, которая живет дольше, чем текущий стек. Я не думаю, что текущий компилятор делает такой глубокий анализ.

Одним из способов избежать затрат на распределение было бы перемещение выделения вне цикла и переназначение значения в выделенную память внутри цикла.

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

[комментарий Рухаха выше] (http://stackoverflow.com/questions/27788813/variadic-functions-causing-unnecessary-heap-allocations-in-go#comment43998000_27788813) более точно по причине. Во всяком случае ваше решение работает, если вызываемая переменная функция не изменяет значения или не хранит указатели для последующего использования, так что это нормально, если OP просто делает Printf, если возникла ошибка. – wldsvc

+0

Царапины, что я сказал, ваш код заставляет копию входящих данных (n1 и n2) на каждой итерации цикла, что в этом случае является субоптимальным. Вероятно, он имеет дело с указателями на структуры, которые намного больше, чем int. Его единственным решением является копирование данных только в блоке экранирования ('if ptr1 == nil ... {') – wldsvc