Распределение стека и куча структур в 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, первый поместил бы объект в кучу, а второй поместил бы его в стек. Первый вернул бы указатель на кучу, второй вернул бы указатель на стек, который испарился бы к моменту возврата функции, что было бы плохо.
Если бы я написал это на Python (или на многих других современных языках, кроме C#), пример 2 был бы невозможен.
Я получаю, что мусор Go собирает оба значения, так что обе вышеуказанные формы в порядке.
Цитировать:
Обратите внимание, что, в отличие от C, вполне нормально возвращать адрес локальной переменной; хранилище, связанное с переменной, сохраняется после возврата из функции. Фактически, взятие адреса составного литерала выделяет новый экземпляр каждый раз, когда он оценивается, поэтому мы можем объединить эти две последние строки.
Но это поднимает пару вопросов.
1 - В примере 1 структура объявлена в куче. Как насчет примера 2? Это объявляется в стеке так же, как это было бы в C, или оно тоже идет в кучу?
2 - Если пример 2 объявлен в стеке, как он остается доступным после возврата функции?
3 - Если пример 2 действительно объявлен в куче, как получается, что структуры передаются по значению, а не по ссылке? Какой смысл указателей в этом случае?
6 ответов
Стоит отметить, что слова "стек" и "куча" не встречаются нигде в спецификации языка. Ваш вопрос сформулирован как "... объявлен в стеке" и "... объявлен в куче", но обратите внимание, что синтаксис объявления 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 МБ.
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
это адрес.
Согласно FAQ Го:
если компилятор не может доказать, что на переменную не ссылаются после возврата из функции, то компилятор должен выделить переменную в куче для сбора мусора, чтобы избежать ошибок висячих указателей.
Вы не всегда знаете, расположена ли ваша переменная в стеке или куче.
...
Если вам нужно знать, где расположены ваши переменные, передайте флаг -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
Вот еще одно обсуждение кучи стека и сборщика мусора в A Guide to the Go Garbage Collector.
Где живут ценности
- выделение стека
- Значения Go без указателей, хранящиеся в локальных переменных, скорее всего, вообще не будут управляться сборщиком мусора Go, и вместо этого Go будет организовывать выделение памяти, привязанной к лексической области, в которой она создана . В общем, это более эффективно, чем полагаться на GC, потому что компилятор Go может заранее определить, когда эта память может быть освобождена, и выдать машинные инструкции для очистки. Обычно мы называем выделение памяти для значений Go таким образом «распределением стека», потому что пространство хранится в стеке горутины.
- выделение кучи
- Говорят, что значения Go, память которых не может быть выделена таким образом, потому что компилятор Go не может определить время их жизни, уходят в кучу . «Куча» может рассматриваться как универсальное средство для выделения памяти, когда нужно куда-то поместить значения Go. Процесс выделения памяти в куче обычно называется «динамическим выделением памяти», поскольку и компилятор, и среда выполнения могут делать очень мало предположений относительно того, как эта память используется и когда ее можно очистить. Вот тут-то и появляется сборщик мусора: это система, которая специально идентифицирует и очищает динамическое выделение памяти.
Есть много причин, по которым значение Go может понадобиться для перехода в кучу. Одной из причин может быть то, что его размер определяется динамически. Рассмотрим, например, резервный массив среза, начальный размер которого определяется переменной, а не константой. Обратите внимание, что экранирование в кучу также должно быть транзитивным: если ссылка на значение Go записывается в другое значение Go, которое уже определено как экранированное, это значение также должно экранироваться.
Анализ побега
Что касается того, как получить доступ к информации из escape-анализа компилятора Go, самый простой способ — использовать флаг отладки, поддерживаемый компилятором Go, который описывает все оптимизации, которые он применил или не применил к некоторому пакету в текстовом формате. Это включает в себя, исчезают ли значения. Попробуйте следующую команду, где [package] — путь к пакету Go.
$ go build -gcflags=-m=3 [package]
Оптимизация для конкретной реализации
Сборщик мусора Go чувствителен к демографии оперативной памяти, потому что сложный граф объектов и указателей ограничивает параллелизм и требует больше работы для сборщика мусора. В результате сборщик мусора содержит несколько оптимизаций для конкретных общих структур. Ниже перечислены наиболее полезные из них для оптимизации производительности.
Значения без указателя отделены от других значений.
В результате может оказаться выгодным исключить указатели из структур данных, которые в них строго не нуждаются, так как это снижает нагрузку на кэш, которую сборщик мусора оказывает на программу. В результате структуры данных, которые полагаются на индексы, а не на значения указателей, хотя и менее типизированы, могут работать лучше. Это стоит делать только в том случае, если ясно, что граф объектов сложный, а сборщик мусора тратит много времени на маркировку и сканирование.
ГХ остановит сканирование значений на последнем указателе в значении.
В результате может оказаться выгодным группировать поля указателей в значениях структурного типа в начале значения. Это стоит делать только в том случае, если ясно, что приложение тратит много времени на маркировку и сканирование. (Теоретически компилятор может делать это автоматически, но это еще не реализовано, а поля структур располагаются так, как написано в исходном коде.)
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() не может быть вызван. Потому что это не значит.
Не важно, в куче или стеке размещена переменная, просто используйте ее. При необходимости защитите его мьютексом или каналом.