Розподіл конструкцій в Stack vs купі в 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 аналіз аналізу покаже вказівник на структуру, що виходить, і тому компілятору доведеться виділити структуру. Я думаю, що нинішня реалізація 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 відсутній, що в деяких випадках було б хорошою справою. Тримайся, хоча, ось 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 Мб.


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є адреса.


Дуже дякую! Оновлено, але я приймаю Sonia через трохи про аналіз втечі.
Джо

1
peterSo, як ви та @Sonia виробляєте цю збірку? Ви обоє мають однакове форматування. Я не можу його виготовити незалежно від команд / прапорів, спробувавши objdump, go tool, otool.
10 клс

3
Ах, зрозумів - gcflags.
10 клс 10

30

Відповідно до поширених запитань Go :

якщо компілятор не може довести, що на змінну не посилається після повернення функції, тоді компілятор повинен виділити змінну на купі зібраного сміття, щоб уникнути помилок вказівника.



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

    ...

    return chunk, nil
}


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

    ...

    return &chunk, nil
}

Функція1 і Функція2 можуть бути вбудованими. І змінна повернення не уникне. Не потрібно виділяти змінну на купу.

Мій приклад коду:

 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 () може не викликатися. Тому що це не дає коштів.

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

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.