Стек и распределение кучи структур в Go и то, как они относятся к сборке мусора
Я новичок в 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, первый бы поставил объект в кучу, а второй поместил бы его в стек. Первый вернет указатель на кучу, второй вернет указатель на стек, который бы испарился к тому времени, когда функция вернулась, что было бы Bad Thing.
Если бы я написал его на Python (или на многих других современных языках, кроме С#), пример 2 не был бы возможен.
Получаю, что Go мусор собирает оба значения, поэтому обе эти формы в порядке.
Цитата:
Обратите внимание, что, в отличие от C, вполне нормально возвращать адрес локальная переменная; сохраняется память, связанная с переменной после возвращения функции. Фактически, принимая адрес составного literal выделяет новый экземпляр каждый раз, когда он оценивается, поэтому мы могут объединить эти последние две строки.
http://golang.org/doc/effective_go.html#functions
Но это вызывает несколько вопросов.
1 - В примере 1 структура объявляется в куче. Как насчет примера 2? Является ли это объявленным в стеке таким же образом, что это будет в C или оно тоже будет в куче?
2 - Если в стеке объявлен пример 2, как он останется доступным после возвращения функции?
3 - Если пример 2 фактически объявлен в куче, как это происходит, то structs передаются по значению, а не по ссылке? Какая точка указателей в этом случае?
Ответы
Ответ 1
Стоит отметить, что слова "стек" и "куча" нигде не отображаются в спецификации языка. Ваш вопрос сформулирован как "... объявлен в стеке" и "... объявлен в куче", но обратите внимание, что синтаксис объявления Go ничего не говорит о стеке или куче.
Это технически делает ответ на все ваши вопросы зависимыми. На самом деле, конечно, есть стек (за goroutine!) И кучу, а некоторые вещи идут в стек, а некоторые - в кучу. В некоторых случаях компилятор следует жестким правилам (например, "new
всегда выделяет кучу" ), а в других компилятор выполняет "анализ побега", чтобы решить, может ли объект жить в стеке или если он должен быть выделен в кучу.
В вашем примере 2, анализ 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 (отлично). Очевидно, у нас работают разные компиляторы. В противном случае, см., Что я modfied myFunction2, чтобы возвращать myStructType, а не * myStructType. Вызов runtime.new отсутствует, что в некоторых случаях будет хорошим. Держитесь хотя, здесь myFunction3,
--- 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, и да, он действительно работает, чтобы вернуть объект 8MB по значению. Он работает, но вы, как правило, этого не хотели. Точка указателя здесь будет заключаться в том, чтобы избежать нажатия вокруг объектов размером 8 МБ.
Ответ 2
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 ,
Calls
В вызове функции значение функции и аргументы вычисляются в обычный порядок. После их оценки параметры вызова передаются по значению функции, и вызываемая функция начинается выполнение. Возвращаемые параметры функции передаются по значению вернуться к вызывающей функции, когда функция вернется.
Все параметры функции и возврата передаются по значению. Значение возвращаемого параметра с типом *MyStructType
является адресом.
Ответ 3
В соответствии с Рекомендуем часто задаваемые вопросы:
если компилятор не может доказать, что переменная не указана после функция возвращает, то компилятор должен распределить переменную на сборку мусора, чтобы избежать оборванных ошибок указателя.
Ответ 4
Вы не всегда знаете, расположена ли ваша переменная в стеке или куче.
...
Если вам нужно знать, где размещены ваши переменные, передайте флаг "-m" "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
Ответ 5
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() может не вызываться. Потому что это не значит.
Не важно, будет ли переменная размещена в куче или стеке, просто используйте ее. Защитите его мьютексом или каналом, если это необходимо.