Alokacja struktur na stosie i na stercie w Go oraz ich związek z wyrzucaniem elementów bezużytecznych

165

Jestem nowy w Go i doświadczam nieco zbieżnego dysonansu między programowaniem opartym na stosie w stylu C, w którym zmienne automatyczne żyją na stosie i przydzieloną pamięć na stercie i programowaniem opartym na stosie w stylu Pythona, gdzie jedyną rzeczą, która żyje na stosie, są odniesienia / wskaźniki do obiektów na stercie.

O ile wiem, dwie następujące funkcje dają ten sam wynik:

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

    ...

    return chunk, nil
}


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

    ...

    return &chunk, nil
}

tj. przydziel nową strukturę i zwróć ją.

Gdybym napisał to w C, pierwszy umieściłby obiekt na stercie, a drugi na stosie. Pierwsza zwróciłaby wskaźnik do stosu, druga zwróciłaby wskaźnik do stosu, który wyparowałby do czasu zwrócenia funkcji, co byłoby złą rzeczą.

Gdybym napisał to w Pythonie (lub wielu innych współczesnych językach oprócz C #), przykład 2 nie byłby możliwy.

Rozumiem, że śmieci Go gromadzą obie wartości, więc obie powyższe formy są w porządku.

Cytować:

Zauważ, że w przeciwieństwie do C, zwrócenie adresu zmiennej lokalnej jest całkowicie OK; pamięć związana ze zmienną przetrwa po powrocie funkcji. W rzeczywistości pobranie adresu literału złożonego przydziela nowe wystąpienie za każdym razem, gdy jest ono oceniane, więc możemy połączyć te dwie ostatnie linie.

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

Ale rodzi kilka pytań.

1 - W przykładzie 1 struktura jest zadeklarowana na stercie. A co z przykładem 2? Czy jest to zadeklarowane na stosie w taki sam sposób, w jaki byłoby to w C, czy też trafia na stos?

2 - Jeśli przykład 2 jest zadeklarowany na stosie, w jaki sposób pozostaje on dostępny po powrocie funkcji?

3 - Jeśli przykład 2 jest faktycznie zadeklarowany na stercie, w jaki sposób struktury są przekazywane przez wartość, a nie przez odwołanie? Jaki jest sens wskazówek w tym przypadku?

Joe
źródło

Odpowiedzi:

170

Warto zauważyć, że słowa „stos” i „sterta” nie pojawiają się nigdzie w specyfikacji języka. Twoje pytanie jest sformułowane w następujący sposób: „… jest zadeklarowane na stosie” i „… zadeklarowane na stercie”, ale pamiętaj, że składnia deklaracji Go nie mówi nic o stosie ani stercie.

To technicznie uzależnia odpowiedź na wszystkie pytania od implementacji. W rzeczywistości oczywiście jest stos (na gorutynę!) I stos, a niektóre rzeczy trafiają na stos, a inne na stos. W niektórych przypadkach kompilator przestrzega sztywnych reguł (takich jak „ newzawsze alokuje na stercie”), aw innych kompilator przeprowadza „analizę ucieczki”, aby zdecydować, czy obiekt może żyć na stosie, czy też musi zostać przydzielony na stercie.

W twoim przykładzie 2 analiza ucieczki pokazałaby wskaźnik do uciekającej struktury, więc kompilator musiałby zaalokować strukturę. Myślę, że obecna implementacja Go w tym przypadku jest zgodna ze sztywną zasadą, która mówi, że jeśli adres jest pobierany z jakiejkolwiek części struktury, struktura przechodzi na stos.

W przypadku pytania 3 grozi nam dezorientacja terminologiczna. Wszystko w Go jest przekazywane przez wartość, nie ma przejścia przez odniesienie. Tutaj zwracasz wartość wskaźnika. Jaki jest sens wskazówek? Rozważ następującą modyfikację Twojego przykładu:

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
}

Zmodyfikowałem myFunction2, aby zwracał strukturę, a nie adres struktury. Porównaj teraz wyjście zespołu myFunction1 i 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     ,

Nie martw się, że wyjście myFunction1 jest inne niż w odpowiedzi peterSO (doskonała). Oczywiście używamy różnych kompilatorów. W przeciwnym razie zobacz, że zmodyfikowałem myFunction2 tak, aby zwracał myStructType zamiast * myStructType. Zniknęło wywołanie runtime.new, co w niektórych przypadkach byłoby dobrym rozwiązaniem. Chwileczkę, oto 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     ,

Wciąż brak wywołania runtime.new i tak, to naprawdę działa, aby zwrócić obiekt o wielkości 8 MB według wartości. Działa, ale zazwyczaj nie chcesz. Celem wskaźnika byłoby uniknięcie przesuwania obiektów o wielkości 8 MB.

Sonia
źródło
9
Wspaniale, dziękuje. Tak naprawdę nie pytałem „jaki jest sens wskaźników”, raczej „jaki jest sens wskaźników, kiedy wartości wydają się zachowywać jak wskaźniki”, a ta sprawa i tak staje się dyskusyjna po twojej odpowiedzi.
Joe
25
Byłoby mile widziane krótkie wyjaśnienie zgromadzenia.
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
}

W obu przypadkach obecne implementacje Go przydzielałyby pamięć dla structtypu MyStructTypena stercie i zwracały jej adres. Funkcje są równoważne; źródło asm kompilatora jest takie samo.

--- 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     ,

Połączenia

W wywołaniu funkcji wartość funkcji i argumenty są oceniane w zwykłej kolejności. Po ich ocenie parametry wywołania są przekazywane przez wartość do funkcji, a wywoływana funkcja rozpoczyna wykonywanie. Zwracane parametry funkcji są przekazywane przez wartość z powrotem do funkcji wywołującej, gdy funkcja zwraca.

Wszystkie parametry funkcji i zwracane są przekazywane przez wartość. Wartość parametru zwracanego z typem *MyStructTypeto adres.

peterSO
źródło
Dziękuję bardzo! Głosowano za, ale akceptuję słowa Sonii ze względu na trochę o analizie ucieczki.
Joe
1
peterSo, jak ty i @Sonia produkujecie ten zestaw? Oboje macie to samo formatowanie. Nie mogę go wyprodukować niezależnie od komend / flag, po wypróbowaniu objdump, go tool, otool.
10 cls
3
Ach, rozumiem - gcflags.
10 cls 10
30

Zgodnie z FAQ Go :

jeśli kompilator nie może udowodnić, że po powrocie funkcji nie ma odwołania do zmiennej, kompilator musi przydzielić zmienną na stercie zebranym bez pamięci, aby uniknąć wiszących błędów wskaźnika.

gchain
źródło
0
func Function1() (*MyStructType, error) {
    var chunk *MyStructType = new(HeaderChunk)

    ...

    return chunk, nil
}


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

    ...

    return &chunk, nil
}

Funkcja 1 i Funkcja 2 mogą być funkcjami wbudowanymi. Zmienna zwracana nie ucieknie. Nie jest konieczne przydzielanie zmiennej na stercie.

Mój przykładowy kod:

 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  }

Zgodnie z wyjściem cmd:

go run -gcflags -m test.go

wynik:

# 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

Jeśli kompilator jest wystarczająco inteligentny, nie można wywołać F1 () F2 () F3 () . Ponieważ to nic nie da.

Nie przejmuj się tym, czy zmienna jest przydzielona na stercie czy na stosie, po prostu jej użyj. W razie potrzeby chroń go za pomocą muteksu lub kanału.

g10guang
źródło