Stapel-Heap-Zuordnung von Strukturen in Go und deren Beziehung zur Speicherbereinigung


165

Ich bin neu in Go und erlebe eine gewisse kongitive Dissonanz zwischen stapelbasierter Programmierung im C-Stil, bei der automatische Variablen auf dem Stapel und zugewiesenem Speicher auf dem Heap gespeichert sind, und stapelbasierter Programmierung im Python-Stil, bei der die Das einzige, was auf dem Stapel lebt, sind Verweise / Zeiger auf Objekte auf dem Heap.

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

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

    ...

    return chunk, nil
}


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

    ...

    return &chunk, nil
}

dh eine neue Struktur zuweisen und zurückgeben.

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 Stapel zurückgeben, der zum Zeitpunkt der Rückkehr der Funktion verdunstet 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, also sind beide oben genannten Formen in Ordnung.

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. Wenn Sie die Adresse eines zusammengesetzten Literals verwenden, wird 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 dem Heap?

2 - Wenn Beispiel 2 auf dem Stapel 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 eher als Wert als als Referenz übergeben werden? Was ist der Sinn von Zeigern in diesem Fall?

Antworten:


170

Es ist erwähnenswert, dass die Wörter "Stapel" und "Haufen" nirgendwo in der Sprachspezifikation vorkommen. Ihre Frage lautet: "... ist 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 technisch abhängig. Tatsächlich gibt es natürlich einen Stapel (pro Goroutine!) Und einen Haufen, und einige Dinge gehen auf den Stapel und einige auf den Haufen. In einigen Fällen folgt der Compiler strengen Regeln (wie " newImmer auf dem Heap zuweisen") und in anderen Fällen führt der Compiler eine "Escape-Analyse" durch, um zu entscheiden, ob ein Objekt auf dem Stapel leben kann oder ob es auf dem Heap zugewiesen werden muss.

In Ihrem Beispiel 2 würde die Escape-Analyse den Zeiger auf die Escape-Struktur anzeigen, sodass der Compiler die Struktur zuweisen müsste. Ich denke, die aktuelle Implementierung von Go folgt in diesem Fall jedoch einer starren Regel: Wenn die Adresse von einem Teil einer Struktur übernommen wird, geht die Struktur auf den Heap.

Bei Frage 3 besteht die Gefahr, dass wir hinsichtlich der Terminologie verwirrt werden. 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 so geändert, dass die Struktur und nicht die Adresse der Struktur zurückgegeben wird. 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     ,

Machen Sie sich keine Sorgen, dass die Ausgabe von myFunction1 hier anders ist als in der (ausgezeichneten) Antwort von peterSO. Wir führen offensichtlich verschiedene Compiler aus. Andernfalls stellen Sie sicher, dass ich myFunction2 so geändert habe, dass myStructType anstelle von * myStructType zurückgegeben wird. Der Aufruf von runtime.new ist weg, was in einigen Fällen eine gute Sache wäre. Moment mal, 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 hier wäre, das Verschieben von 8-MB-Objekten zu vermeiden.


9
Großartig, danke. Ich habe nicht wirklich gefragt "Was ist der Sinn von Zeigern überhaupt", sondern eher "Was ist der Sinn von Zeigern, wenn sich Werte wie Zeiger zu verhalten scheinen", und dieser Fall wird durch Ihre Antwort sowieso in Frage gestellt.
Joe

25
Eine kurze Erklärung der Montage wäre willkommen.
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
}

In beiden Fällen würden aktuelle Implementierungen von Go Speicher für einen structTyp MyStructTypeauf einem Heap zuweisen und dessen Adresse zurückgeben. Die Funktionen sind äquivalent; Die Quelle des Compilers 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 ihrer 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ückkehr der Funktion als Wert an die aufrufende Funktion zurückgegeben.

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


Vielen Dank! Upvoted, aber ich akzeptiere Sonia wegen der Fluchtanalyse.
Joe

1
peterSo, wie produzieren Sie und @Sonia diese Baugruppe? Sie haben beide die gleiche Formatierung. Ich kann es nicht produzieren, unabhängig von Befehl / Flags, nachdem ich objdump, go tool, otool ausprobiert habe.
10 cls

3
Ah, verstanden - gcflags.
10 cls

30

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 Müll gesammelten Heap zuweisen, um baumelnde Zeigerfehler zu vermeiden.



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

    ...

    return chunk, nil
}


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

    ...

    return &chunk, nil
}

Funktion1 und Funktion2 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, wird F1 () F2 () F3 () möglicherweise nicht aufgerufen. Weil es keine Mittel gibt.

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

Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.