wake-up-neo.com

Stack vs. Heap-Zuweisung von Strukturen in Go und deren Beziehung zur Garbage Collection

Ich bin neu in Go und habe eine gewisse Dissonanz zwischen der Stapelprogrammierung im C-Stil, bei der automatische Variablen auf dem Stapel und zugeordneter Speicher auf dem Heap und der Stapelprogrammierung im Python-Stil gespeichert sind das einzige, was auf dem Stapel lebt, sind Verweise/Zeiger auf Objekte auf dem Haufen.

Soweit ich das beurteilen kann, geben die beiden folgenden Funktionen dieselbe Ausgabe aus:

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

    ...

    return chunk, nil
}


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

    ...

    return &chunk, nil
}

zuweisen einer neuen Struktur und Zurückgeben derselben.

Wenn ich das in C geschrieben hätte, hätte der erste ein Objekt auf den Haufen gelegt und der zweite hätte es auf den Stapel gelegt. Der erste würde einen Zeiger auf den Heap zurückgeben, der zweite würde einen Zeiger auf den Stack zurückgeben, der zum Zeitpunkt der Rückkehr der Funktion verflogen wäre, was eine schlechte Sache wäre.

Wenn ich es in Python (oder vielen anderen modernen Sprachen außer C #) geschrieben hätte, wäre Beispiel 2 nicht möglich gewesen.

Ich verstehe, dass Go Garbage beide Werte sammelt, sodass beide oben genannten Formen in Ordnung sind.

Zitieren:

Beachten Sie, dass es im Gegensatz zu C vollkommen in Ordnung ist, die Adresse einer lokalen Variablen zurückzugeben. Der der Variablen zugeordnete Speicher bleibt nach der Rückkehr der Funktion erhalten. Tatsächlich wird durch die Angabe der Adresse eines zusammengesetzten Literal bei jeder Auswertung eine neue Instanz zugewiesen, sodass wir diese beiden letzten Zeilen kombinieren können.

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

Aber es wirft ein paar Fragen auf.

1 - In Beispiel 1 wird die Struktur auf dem Heap deklariert. Was ist mit Beispiel 2? Wird das auf dem Stack genauso deklariert wie in C oder geht es auch auf den Haufen?

2 - Wenn Beispiel 2 im Stack deklariert ist, wie bleibt es verfügbar, nachdem die Funktion zurückgegeben wurde?

3 - Wenn Beispiel 2 tatsächlich auf dem Heap deklariert ist, wie kommt es dann, dass Strukturen als Wert und nicht als Referenz übergeben werden? Was ist der Sinn von Zeigern in diesem Fall?

141
Joe

Es ist erwähnenswert, dass die Wörter "Stapel" und "Haufen" nirgendwo in der Sprachspezifikation vorkommen. Ihre Frage lautet wie folgt: "... wird auf dem Stapel deklariert" und "... auf dem Heap deklariert". Beachten Sie jedoch, dass die Syntax der Go-Deklaration nichts über Stapel oder Heap aussagt.

Das macht die Beantwortung all Ihrer Fragen umsetzungsabhängig. Tatsächlich gibt es natürlich einen Stapel (per Goroutine!) Und einen Haufen und einige Dinge gehen auf den Stapel und einige auf den Haufen. In einigen Fällen folgt der Compiler starren Regeln (wie "new reserviert immer auf dem Heap"), und in anderen Fällen führt der Compiler eine "Escape-Analyse" durch, um zu entscheiden, ob ein Objekt auf dem Stack vorhanden sein kann oder zugewiesen werden muss auf dem Haufen.

In Ihrem Beispiel 2 würde die Escape-Analyse den Zeiger auf die Struktur zeigen, die escapt, und der Compiler müsste die Struktur zuordnen. Ich denke, dass die aktuelle Implementierung von Go in diesem Fall einer starren Regel folgt: Wenn die Adresse eines Teils einer Struktur genommen wird, geht die Struktur auf den Heap.

Bei Frage 3 besteht die Gefahr, dass wir uns über die Terminologie wundern. Alles in Go wird als Wert übergeben, es gibt keine Referenzübergabe. Hier geben Sie einen Zeigerwert zurück. Was ist der Sinn von Zeigern? Betrachten Sie die folgende Änderung Ihres Beispiels:

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
}

Ich habe myFunction2 geändert, um die Struktur und nicht die Adresse der Struktur zurückzugeben. Vergleichen Sie jetzt die Assembly-Ausgabe von myFunction1 und 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     ,

Mach dir keine Sorgen, dass die Ausgabe von myFunction1 hier anders ist als in der (ausgezeichneten) Antwort von peterSO. Wir betreiben offensichtlich verschiedene Compiler. Ansonsten vergewissere dich, dass ich myFunction2 so geändert habe, dass myStructType und nicht * myStructType zurückgegeben wird. Der Aufruf von runtime.new ist weg, was in manchen Fällen gut wäre. Warte einen Moment, hier ist 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     ,

Immer noch kein Aufruf von runtime.new, und ja, es funktioniert wirklich, ein 8-MB-Objekt nach Wert zurückzugeben. Es funktioniert, aber normalerweise möchten Sie es nicht. Der Punkt eines Zeigers ist hier, um zu vermeiden, dass 8 MB große Objekte verschoben werden.

149
Sonia
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
}

In beiden Fällen würden aktuelle Implementierungen von Go Speicher für einen struct vom Typ MyStructType auf einem Heap reservieren und dessen Adresse zurückgeben. Die Funktionen sind gleichwertig; Die Compiler-ASM-Quelle ist dieselbe.

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

Anrufe

Bei einem Funktionsaufruf werden der Funktionswert und die Argumente in der üblichen Reihenfolge ausgewertet. Nach der Auswertung werden die Parameter des Aufrufs als Wert an die Funktion übergeben und die aufgerufene Funktion beginnt mit der Ausführung. Die Rückgabeparameter der Funktion werden bei der Rückgabe der Funktion als Wert an die aufrufende Funktion zurückgegeben.

Alle Funktions- und Rückgabeparameter werden als Wert übergeben. Der Rückgabeparameterwert mit dem Typ *MyStructType ist eine Adresse.

55
peterSO

Laut Go's FAQ :

wenn der Compiler nach der Rückkehr der Funktion nicht nachweisen kann, dass auf die Variable nicht verwiesen wird, muss der Compiler die Variable auf dem durch Speicherbereinigung gesammelten Heap zuordnen, um baumelnde Zeigerfehler zu vermeiden.

25
gchain

Sie wissen nicht immer, ob Ihre Variable auf dem Stapel oder Heap zugeordnet ist.
...
Wenn Sie wissen möchten, wo Ihre Variablen zugewiesen sind, übergeben Sie das GC-Flag "-m", um "Build" oder "Run" auszuführen (z. B. go run -gcflags -m app.go).

Quelle: http://devs.cloudimmunity.com/gotchas-and-common-mistakes-in-go-golang/index.html#stack_heap_vars

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

    ...

    return chunk, nil
}


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

    ...

    return &chunk, nil
}

Function1 und Function2 können Inline-Funktionen sein. Und die Rückgabevariable wird nicht entkommen. Es ist nicht erforderlich, eine Variable auf dem Heap zuzuweisen.

Mein Beispielcode:

 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  }

Laut Ausgabe von cmd:

go run -gcflags -m test.go

ausgabe:

# 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

Wenn der Compiler intelligent genug ist, kann F1 ()F2 ()F3 () möglicherweise nicht aufgerufen werden. Weil es keine Mittel macht.

Es ist egal, ob eine Variable auf einem Heap oder einem Stack zugeordnet ist, verwenden Sie sie einfach. Schützen Sie es bei Bedarf durch Mutex oder Kanal.

0
g10guang