Allocazione di stack e heap delle strutture in Go e in che modo sono correlate alla garbage collection


165

Sono nuovo di Go e sto sperimentando un po 'di dissonanza congestiva tra la programmazione basata sullo stack in stile C in cui le variabili automatiche vivono nello stack e la memoria allocata vive nell'heap e la programmazione basata sullo stack in stile Python in cui il l'unica cosa che vive nello stack sono riferimenti / puntatori agli oggetti nell'heap.

Per quanto ne so, le due seguenti funzioni danno lo stesso output:

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

    ...

    return chunk, nil
}


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

    ...

    return &chunk, nil
}

cioè allocare una nuova struttura e restituirla.

Se lo avessi scritto in C, il primo avrebbe messo un oggetto nell'heap e il secondo l'avrebbe messo in pila. Il primo avrebbe restituito un puntatore all'heap, il secondo avrebbe restituito un puntatore allo stack, che sarebbe evaporato al momento del ritorno della funzione, che sarebbe una cosa negativa.

Se l'avessi scritto in Python (o in molte altre lingue moderne tranne C #), l'esempio 2 non sarebbe stato possibile.

Ho capito che Go Garbage raccoglie entrambi i valori, quindi entrambi i moduli sopra vanno bene.

Per citare:

Si noti che, a differenza di C, è perfettamente OK restituire l'indirizzo di una variabile locale; la memoria associata alla variabile sopravvive dopo il ritorno della funzione. In effetti, prendere l'indirizzo di un letterale composito alloca una nuova istanza ogni volta che viene valutato, quindi possiamo combinare queste ultime due righe.

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

Ma solleva un paio di domande.

1 - Nell'esempio 1, la struttura è dichiarata nell'heap. Che dire dell'esempio 2? È dichiarato nello stack nello stesso modo in cui verrebbe in C o va anche nell'heap?

2 - Se l'esempio 2 viene dichiarato nello stack, come rimane disponibile dopo il ritorno della funzione?

3 - Se l'esempio 2 viene effettivamente dichiarato sull'heap, come mai le strutture vengono passate per valore anziché per riferimento? Qual è il punto di puntatori in questo caso?

Risposte:


170

Vale la pena notare che le parole "stack" e "heap" non compaiono da nessuna parte nelle specifiche della lingua. La tua domanda è formulata con "... è dichiarata nello stack" e "... dichiarata nell'heap", ma nota che la sintassi della dichiarazione Go non dice nulla su stack o heap.

Ciò rende tecnicamente dipendente la risposta a tutte le tue domande. In realtà, naturalmente, c'è uno stack (per goroutine!) E un heap e alcune cose vanno nello stack e altre nello heap. In alcuni casi il compilatore segue regole rigide (come " newalloca sempre sullo heap") e in altri il compilatore esegue "analisi di escape" per decidere se un oggetto può vivere nello stack o se deve essere allocato sullo heap.

Nel tuo esempio 2, l'analisi di escape mostrerebbe il puntatore alla struttura di escape e quindi il compilatore dovrebbe allocare la struttura. Penso che l'attuale implementazione di Go segua una rigida regola in questo caso, che è che se l'indirizzo viene preso da qualsiasi parte di una struttura, la struttura va nel mucchio.

Per la domanda 3, rischiamo di confonderci sulla terminologia. Tutto in Go viene passato per valore, non c'è passaggio per riferimento. Qui stai restituendo un valore di puntatore. Qual è il punto di puntatori? Considera la seguente modifica del tuo esempio:

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
}

Ho modificato myFunction2 per restituire la struttura anziché l'indirizzo della struttura. Confronta ora l'output dell'assembly di myFunction1 e 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     ,

Non preoccuparti che l'output di myFunction1 qui sia diverso rispetto alla risposta (eccellente) di peterSO. Ovviamente stiamo eseguendo compilatori diversi. Altrimenti, vedi che ho modificato myFunction2 per restituire myStructType anziché * myStructType. La chiamata a runtime.new è sparita, il che in alcuni casi sarebbe una buona cosa. Aspetta però, ecco 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     ,

Ancora nessuna chiamata a runtime.new e sì, funziona davvero per restituire un oggetto da 8 MB in base al valore. Funziona, ma di solito non vorresti. Il punto di un puntatore qui sarebbe quello di evitare di spingere oggetti di 8 MB.


9
Eccellente grazie. Non stavo davvero chiedendo "qual è il punto dei puntatori", era più simile a "qual è il punto dei puntatori quando i valori sembrano comportarsi come puntatori", e quel caso è reso discutibile dalla tua risposta comunque.
Joe

25
Una breve spiegazione dell'assemblea sarebbe apprezzata.
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 entrambi i casi, le attuali implementazioni di Go allocarebbero memoria per un structtipo MyStructTypesu un heap e restituirebbero il suo indirizzo. Le funzioni sono equivalenti; il compilatore asm source è lo stesso.

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

chiamate

In una chiamata di funzione, il valore della funzione e gli argomenti vengono valutati nell'ordine normale. Dopo che sono stati valutati, i parametri della chiamata vengono passati per valore alla funzione e la funzione chiamata inizia l'esecuzione. I parametri di ritorno della funzione vengono restituiti per valore alla funzione chiamante quando la funzione ritorna.

Tutti i parametri di funzione e di ritorno vengono passati per valore. Il valore del parametro restituito con type *MyStructTypeè un indirizzo.


Grazie mille! Eseguito l'upgrade, ma sto accettando Sonia per via dell'analisi dell'evasione.
Joe,

1
peter Quindi, come state producendo voi e @Sonia quell'assemblea? Entrambi avete la stessa formattazione. Non riesco a produrlo indipendentemente dal comando / flag, dopo aver provato objdump, go tool, otool.
10 cls,

3
Ah, capito - gcflags.
10 cl 10

30

Secondo le FAQ di Go :

se il compilatore non è in grado di provare che alla variabile non viene fatto riferimento dopo il ritorno della funzione, il compilatore deve allocare la variabile sull'heap raccolto in modo inutile per evitare errori puntatori pendenti.



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

    ...

    return chunk, nil
}


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

    ...

    return &chunk, nil
}

Function1 e Function2 possono essere funzioni incorporate. E la variabile return non sfuggirà. Non è necessario allocare una variabile sull'heap.

Il mio codice di esempio:

 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  }

Secondo l'output di cmd:

go run -gcflags -m test.go

produzione:

# 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

Se il compilatore è abbastanza intelligente, potrebbe non essere possibile chiamare F1 () F2 () F3 () . Perché non fa alcun mezzo.

Non importa se una variabile è allocata su heap o stack, basta usarla. Proteggilo con mutex o canale se necessario.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.