Распределение стека и куча структур в Go и как они связаны со сборкой мусора

165

Я новичок в Go и испытываю небольшой диссонанс между программированием на основе стека в стиле C, где автоматические переменные живут в стеке, а выделенная память - в куче, и программированием на основе стека в стиле Python, где Единственное, что живет в стеке - это ссылки / указатели на объекты в куче.

Насколько я могу судить, две следующие функции дают одинаковый вывод:

func myFunction() (*MyStructType, error) {
    var chunk *MyStructType = new(HeaderChunk)

    ...

    return chunk, nil
}


func myFunction() (*MyStructType, error) {
    var chunk MyStructType

    ...

    return &chunk, nil
}

т.е. выделите новую структуру и верните ее.

Если бы я написал это в C, первый поместил бы объект в кучу, а второй поместил бы его в стек. Первый вернет указатель на кучу, второй вернет указатель на стек, который испарился бы к моменту возврата функции, что было бы плохо.

Если бы я написал это на Python (или на многих других современных языках, кроме C #), пример 2 был бы невозможен.

Я получаю, что мусор Go собирает оба значения, так что обе вышеуказанные формы в порядке.

Цитировать:

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

http://golang.org/doc/effective_go.html#functions

Но это поднимает пару вопросов.

1 - В примере 1 структура объявлена ​​в куче. Как насчет примера 2? Это объявляется в стеке так же, как в C, или оно тоже идет в кучу?

2 - Если пример 2 объявлен в стеке, как он остается доступным после возврата функции?

3 - Если пример 2 действительно объявлен в куче, как получается, что структуры передаются по значению, а не по ссылке? Какой смысл указателей в этом случае?

Джо
источник

Ответы:

170

Стоит отметить, что слова «стек» и «куча» не встречаются нигде в спецификации языка. Ваш вопрос сформулирован как "... объявлен в стеке" и "... объявлен в куче", но обратите внимание, что синтаксис объявления Go ничего не говорит о стеке или куче.

Это технически делает ответ на все ваши вопросы зависимыми от реализации. На самом деле, конечно, есть стек (для каждой процедуры!) И куча, и некоторые вещи идут в стек, а некоторые - в кучу. В некоторых случаях компилятор следует строгим правилам (например, « newвсегда выделяет в куче»), а в других компилятор «избегает анализа», чтобы решить, может ли объект жить в стеке или он должен быть размещен в куче.

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

На вопрос 3 мы рискуем запутаться в терминологии. Все в Go передается по значению, по ссылке нет передачи. Здесь вы возвращаете значение указателя. Какой смысл указателей? Рассмотрим следующую модификацию вашего примера:

type MyStructType struct{}

func myFunction1() (*MyStructType, error) {
    var chunk *MyStructType = new(MyStructType)
    // ...
    return chunk, nil
}

func myFunction2() (MyStructType, error) {
    var chunk MyStructType
    // ...
    return chunk, nil
}

type bigStruct struct {
    lots [1e6]float64
}

func myFunction3() (bigStruct, error) {
    var chunk bigStruct
    // ...
    return chunk, nil
}

Я изменил myFunction2, чтобы вернуть структуру, а не адрес структуры. Сравните вывод сборки myFunction1 и myFunction2 сейчас,

--- prog list "myFunction1" ---
0000 (s.go:5) TEXT    myFunction1+0(SB),$16-24
0001 (s.go:6) MOVQ    $type."".MyStructType+0(SB),(SP)
0002 (s.go:6) CALL    ,runtime.new+0(SB)
0003 (s.go:6) MOVQ    8(SP),AX
0004 (s.go:8) MOVQ    AX,.noname+0(FP)
0005 (s.go:8) MOVQ    $0,.noname+8(FP)
0006 (s.go:8) MOVQ    $0,.noname+16(FP)
0007 (s.go:8) RET     ,

--- prog list "myFunction2" ---
0008 (s.go:11) TEXT    myFunction2+0(SB),$0-16
0009 (s.go:12) LEAQ    chunk+0(SP),DI
0010 (s.go:12) MOVQ    $0,AX
0011 (s.go:14) LEAQ    .noname+0(FP),BX
0012 (s.go:14) LEAQ    chunk+0(SP),BX
0013 (s.go:14) MOVQ    $0,.noname+0(FP)
0014 (s.go:14) MOVQ    $0,.noname+8(FP)
0015 (s.go:14) RET     ,

Не беспокойтесь, что вывод myFunction1 здесь отличается от ответа peterSO (отлично). Мы явно запускаем разные компиляторы. В противном случае убедитесь, что я изменил myFunction2, чтобы он возвращал myStructType, а не * myStructType. Вызван runtime.new, что в некоторых случаях было бы хорошо. Держись хотя, вот моя функция3,

--- prog list "myFunction3" ---
0016 (s.go:21) TEXT    myFunction3+0(SB),$8000000-8000016
0017 (s.go:22) LEAQ    chunk+-8000000(SP),DI
0018 (s.go:22) MOVQ    $0,AX
0019 (s.go:22) MOVQ    $1000000,CX
0020 (s.go:22) REP     ,
0021 (s.go:22) STOSQ   ,
0022 (s.go:24) LEAQ    chunk+-8000000(SP),SI
0023 (s.go:24) LEAQ    .noname+0(FP),DI
0024 (s.go:24) MOVQ    $1000000,CX
0025 (s.go:24) REP     ,
0026 (s.go:24) MOVSQ   ,
0027 (s.go:24) MOVQ    $0,.noname+8000000(FP)
0028 (s.go:24) MOVQ    $0,.noname+8000008(FP)
0029 (s.go:24) RET     ,

До сих пор нет вызова runtime.new, и да, он действительно работает, чтобы возвратить 8 МБ объект по значению. Это работает, но вы обычно не хотели бы. Точка указателя здесь должна была бы избегать проталкивания объектов размером 8 МБ.

Соня
источник
9
Отлично, спасибо. Я на самом деле не спрашивал «в чем смысл вообще указателей», это было больше похоже на «в чем смысл указателей, когда значения, похоже, ведут себя как указатели», и этот случай все равно представляется спорным из-за вашего ответа.
Джо
25
Краткое объяснение сборки будет оценено.
ElefEnt
59
type MyStructType struct{}

func myFunction1() (*MyStructType, error) {
    var chunk *MyStructType = new(MyStructType)
    // ...
    return chunk, nil
}

func myFunction2() (*MyStructType, error) {
    var chunk MyStructType
    // ...
    return &chunk, nil
}

В обоих случаях текущие реализации Go выделяют память для structтипа MyStructTypeв куче и возвращают ее адрес. Функции эквивалентны; источник asm компилятора такой же.

--- prog list "myFunction1" ---
0000 (temp.go:9) TEXT    myFunction1+0(SB),$8-12
0001 (temp.go:10) MOVL    $type."".MyStructType+0(SB),(SP)
0002 (temp.go:10) CALL    ,runtime.new+0(SB)
0003 (temp.go:10) MOVL    4(SP),BX
0004 (temp.go:12) MOVL    BX,.noname+0(FP)
0005 (temp.go:12) MOVL    $0,AX
0006 (temp.go:12) LEAL    .noname+4(FP),DI
0007 (temp.go:12) STOSL   ,
0008 (temp.go:12) STOSL   ,
0009 (temp.go:12) RET     ,

--- prog list "myFunction2" ---
0010 (temp.go:15) TEXT    myFunction2+0(SB),$8-12
0011 (temp.go:16) MOVL    $type."".MyStructType+0(SB),(SP)
0012 (temp.go:16) CALL    ,runtime.new+0(SB)
0013 (temp.go:16) MOVL    4(SP),BX
0014 (temp.go:18) MOVL    BX,.noname+0(FP)
0015 (temp.go:18) MOVL    $0,AX
0016 (temp.go:18) LEAL    .noname+4(FP),DI
0017 (temp.go:18) STOSL   ,
0018 (temp.go:18) STOSL   ,
0019 (temp.go:18) RET     ,

Вызовы

При вызове функции значение и аргументы функции оцениваются в обычном порядке. После оценки параметры вызова передаются функции по значению, и вызываемая функция начинает выполнение. Возвращаемые параметры функции передаются по значению обратно в вызывающую функцию, когда функция возвращается.

Все функции и возвращаемые параметры передаются по значению. Возвращаемое значение параметра с типом *MyStructTypeявляется адресом.

peterSO
источник
Огромное спасибо! Проголосовал, но я принимаю Соню из-за того, что немного разбираюсь в анализе побега.
Джо
1
peterSo, как вы и @Sonia создаете эту сборку? У вас обоих одинаковое форматирование. Я не могу произвести это независимо от команды / флагов, попробовав objdump, go tool, otool.
10 часов
3
Ах, понял - gcflags.
10 cls 10
30

Согласно FAQ Го :

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

gchain
источник
11

Вы не всегда знаете, расположена ли ваша переменная в стеке или куче.
...
Если вам нужно знать, где расположены ваши переменные, передайте флаг -m "gm" в "go build" или "go run" (например, go run -gcflags -m app.go).

Источник: http://devs.cloudimmunity.com/gotchas-and-common-mistakes-in-go-golang/index.html#stack_heap_vars

пользователь
источник
0
func Function1() (*MyStructType, error) {
    var chunk *MyStructType = new(HeaderChunk)

    ...

    return chunk, nil
}


func Function2() (*MyStructType, error) {
    var chunk MyStructType

    ...

    return &chunk, nil
}

Function1 и Function2 могут быть встроенными функциями. И возвращаемая переменная не уйдет. Нет необходимости размещать переменную в куче.

Мой пример кода:

 1  package main
 2  
 3  type S struct {
 4          x int
 5  }
 6  
 7  func main() {
 8          F1()
 9          F2()
10          F3()
11  }
12  
13  func F1() *S {
14          s := new(S)
15          return s
16  }
17  
18  func F2() *S {
19          s := S{x: 10}
20          return &s
21  }
22  
23  func F3() S {
24          s := S{x: 9}
25          return s
26  }

По выводу cmd:

go run -gcflags -m test.go

вывод:

# command-line-arguments
./test.go:13:6: can inline F1
./test.go:18:6: can inline F2
./test.go:23:6: can inline F3
./test.go:7:6: can inline main
./test.go:8:4: inlining call to F1
./test.go:9:4: inlining call to F2
./test.go:10:4: inlining call to F3
/var/folders/nr/lxtqsz6x1x1gfbyp1p0jy4p00000gn/T/go-build333003258/b001/_gomod_.go:6:6: can inline init.0
./test.go:8:4: main new(S) does not escape
./test.go:9:4: main &s does not escape
./test.go:14:10: new(S) escapes to heap
./test.go:20:9: &s escapes to heap
./test.go:19:2: moved to heap: s

Если компилятор достаточно умен, F1 () F2 () F3 () может не вызываться. Потому что это не значит.

Не важно, будет ли переменная размещена в куче или стеке, просто используйте ее. Защитите его мьютексом или каналом, если это необходимо.

g10guang
источник