Allocation de pile ou de tas de structures dans Go, et comment elles sont liées au garbage collection

165

Je suis nouveau dans Go et j'éprouve un peu de dissonance congitive entre la programmation basée sur la pile de style C où les variables automatiques vivent sur la pile et la mémoire allouée vit sur le tas et et la programmation basée sur la pile de style Python où le la seule chose qui vit sur la pile sont des références / pointeurs vers des objets sur le tas.

Pour autant que je sache, les deux fonctions suivantes donnent le même résultat:

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

    ...

    return chunk, nil
}


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

    ...

    return &chunk, nil
}

c'est-à-dire allouer une nouvelle structure et la renvoyer.

Si j'avais écrit cela en C, le premier aurait mis un objet sur le tas et le second l'aurait mis sur la pile. Le premier renverrait un pointeur vers le tas, le second renverrait un pointeur vers la pile, qui se serait évaporé au moment du retour de la fonction, ce qui serait une mauvaise chose.

Si je l'avais écrit en Python (ou dans de nombreux autres langages modernes à l'exception de C #), l'exemple 2 n'aurait pas été possible.

Je comprends que Go garbage collecte les deux valeurs, donc les deux formulaires ci-dessus conviennent.

Citer:

Notez que, contrairement à C, il est parfaitement correct de renvoyer l'adresse d'une variable locale; le stockage associé à la variable survit après le retour de la fonction. En fait, prendre l'adresse d'un littéral composite alloue une nouvelle instance à chaque fois qu'elle est évaluée, nous pouvons donc combiner ces deux dernières lignes.

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

Mais cela soulève quelques questions.

1 - Dans l'exemple 1, la structure est déclarée sur le tas. Qu'en est-il de l'exemple 2? Est-ce que cela est déclaré sur la pile de la même manière qu'il le serait en C ou est-ce que cela va aussi sur le tas?

2 - Si l'exemple 2 est déclaré sur la pile, comment reste-t-il disponible après le retour de la fonction?

3 - Si l'exemple 2 est effectivement déclaré sur le tas, comment se fait-il que les structures soient passées par valeur plutôt que par référence? Quel est l'intérêt des pointeurs dans ce cas?

Joe
la source

Réponses:

170

Il convient de noter que les mots «pile» et «tas» n'apparaissent nulle part dans la spécification du langage. Votre question est formulée par "... est déclaré sur la pile" et "... déclaré sur le tas", mais notez que la syntaxe de déclaration Go ne dit rien sur la pile ou le tas.

Cela rend techniquement la réponse à toutes vos questions de mise en œuvre dépendante. En réalité, bien sûr, il y a une pile (par goroutine!) Et un tas et certaines choses vont sur la pile et d'autres sur le tas. Dans certains cas, le compilateur suit des règles rigides (comme " newalloue toujours sur le tas") et dans d'autres, le compilateur effectue une "analyse d'échappement" pour décider si un objet peut vivre sur la pile ou s'il doit être alloué sur le tas.

Dans votre exemple 2, l'analyse d'échappement afficherait le pointeur vers la structure d'échappement et ainsi le compilateur devrait allouer la structure. Je pense que l'implémentation actuelle de Go suit une règle rigide dans ce cas, à savoir que si l'adresse est prise d'une partie quelconque d'une structure, la structure va sur le tas.

Pour la question 3, nous risquons de nous perdre sur la terminologie. Tout dans Go est passé par valeur, il n'y a pas de passage par référence. Ici, vous renvoyez une valeur de pointeur. Quel est l'intérêt des pointeurs? Considérez la modification suivante de votre exemple:

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
}

J'ai modifié myFunction2 pour renvoyer la structure plutôt que l'adresse de la structure. Comparez maintenant la sortie d'assemblage de myFunction1 et 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     ,

Ne vous inquiétez pas, la sortie de myFunction1 ici est différente de celle de la (excellente) réponse de peterSO. Nous utilisons évidemment différents compilateurs. Sinon, voyez que j'ai modifié myFunction2 pour renvoyer myStructType plutôt que * myStructType. L'appel à runtime.new est parti, ce qui dans certains cas serait une bonne chose. Tenez bon, voici 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     ,

Toujours aucun appel à runtime.new, et oui, cela fonctionne vraiment pour renvoyer un objet de 8 Mo par valeur. Cela fonctionne, mais vous ne le voudriez généralement pas. Le point d'un pointeur ici serait d'éviter de pousser des objets de 8 Mo.

Sonia
la source
9
Excellent, merci. Je ne demandais pas vraiment «à quoi servent les pointeurs du tout», c'était plutôt «à quoi servent les pointeurs lorsque les valeurs semblent se comporter comme des pointeurs», et ce cas est de toute façon rendu sans objet par votre réponse.
Joe
25
Une brève explication de l'assemblage serait appréciée.
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
}

Dans les deux cas, les implémentations actuelles de Go alloueraient de la mémoire pour un structtype MyStructTypesur un tas et renverraient son adresse. Les fonctions sont équivalentes; la source asm du compilateur est la même.

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

Appels

Lors d'un appel de fonction, la valeur et les arguments de la fonction sont évalués dans l'ordre habituel. Après leur évaluation, les paramètres de l'appel sont passés par valeur à la fonction et la fonction appelée commence l'exécution. Les paramètres de retour de la fonction sont renvoyés par valeur à la fonction appelante lorsque la fonction est renvoyée.

Tous les paramètres de fonction et de retour sont passés par valeur. La valeur du paramètre de retour avec type *MyStructTypeest une adresse.

peterSO
la source
Merci beaucoup! Vote positif, mais j'accepte celui de Sonia à cause de l'analyse des évasions.
Joe
1
peterSo, comment vous et @Sonia produisez-vous cet assemblage? Vous avez tous les deux le même formatage. Je ne peux pas le produire indépendamment de la commande / des indicateurs, après avoir essayé objdump, go tool, otool.
10 cls
3
Ah, compris - gcflags.
10 cls 10
30

Selon la FAQ de Go :

si le compilateur ne peut pas prouver que la variable n'est pas référencée après le retour de la fonction, le compilateur doit allouer la variable sur le tas récupéré pour éviter les erreurs de pointeur pendantes.

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

    ...

    return chunk, nil
}


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

    ...

    return &chunk, nil
}

Function1 et Function2 peuvent être des fonctions en ligne. Et la variable de retour ne s'échappera pas. Il n'est pas nécessaire d'allouer une variable sur le tas.

Mon exemple de code:

 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  }

Selon la sortie de cmd:

go run -gcflags -m test.go

production:

# 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

Si le compilateur est assez intelligent, F1 () F2 () F3 () peut ne pas être appelé. Parce que ça ne fait aucun moyen.

Peu importe si une variable est allouée sur un tas ou une pile, utilisez-la simplement. Protégez-le par mutex ou canal si nécessaire.

g10guang
la source