web-dev-qa-db-fra.com

Stack vs tas allocation de structs dans Go, et leur lien avec la récupération de place

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

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 retourner.

Si j'avais écrit ça 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 sur le tas, le second renverrait un pointeur sur la pile, qui se serait évaporé au moment du retour de la fonction, ce qui serait une mauvaise chose.

Si je l'avais écrit dans Python (ou 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 formes ci-dessus conviennent.

Citer:

Notez que, contrairement à C, il est parfaitement correct de renvoyer l’adresse d’une variable locale; la mémoire associée à 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'il est évalué, afin de pouvoir 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 que ce 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 réellement déclaré sur le tas, comment se fait-il que les structs sont passés par valeur plutôt que par référence? Quel est le point de repère dans ce cas?

141
Joe

Il est à noter que les mots "stack" et "heap" n'apparaissent nulle part dans la spécification de langue. Votre question est libellée avec "... est déclaré sur la pile" et "... déclaré sur le tas", mais notez que la syntaxe de la déclaration Go ne dit rien à propos de la pile ou du tas.

Techniquement, la réponse à toutes vos questions dépend de la mise en œuvre. 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 strictes (comme "new alloue toujours sur le tas") et dans d'autres cas, il effectue une "analyse d'échappement" pour décider si un objet peut vivre dans la pile ou s'il doit être alloué. sur le tas.

Dans votre exemple 2, l'analyse d'échappement afficherait le pointeur sur l'échappement de structure et le compilateur devrait donc allouer la structure. Je pense que la mise en œuvre actuelle de Go suit une règle rigide dans ce cas cependant, à savoir que si l'adresse est prise à partir de n'importe quelle partie d'une structure, la structure continue sur le tas.

Pour la question 3, nous risquons de nous perdre dans la terminologie. Tout dans Go est passé par valeur, il n'y a pas de passe par référence. Ici, vous retournez une valeur de pointeur. Quel est le point de 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 la sortie d'assemblage de myFunction1 et myFunction2 maintenant,

--- 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 réponse (excellente) de peterSO. Nous utilisons évidemment différents compilateurs. Sinon, vérifiez que j'ai modifié myFunction2 pour renvoyer myStructType plutôt que * myStructType. L'appel à runtime.new a disparu, ce qui dans certains cas serait une bonne chose. Tiens 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 pas d’appel à runtime.new, et oui, cela fonctionne vraiment pour renvoyer un objet de 8 Mo par valeur. Cela fonctionne, mais vous ne voudriez généralement pas. L’intérêt d’un pointeur ici est d’éviter de déplacer des objets de 8 Mo environ.

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
}

Dans les deux cas, les implémentations actuelles de Go alloueraient de la mémoire pour un struct de type MyStructType sur un segment de mémoire et renverraient son adresse. Les fonctions sont équivalentes; le compilateur asm source est le 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

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

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

55
peterSO

Selon Go's FAQ :

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

25
gchain

Vous ne savez pas toujours si votre variable est allouée sur la pile ou le tas.
...
Si vous avez besoin de savoir où vos variables sont affectées, passez le drapeau "-m" gc pour "construire" ou "exécuter" (par exemple, go run -gcflags -m app.go).

Source: 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 et Function2 peuvent être des fonctions en ligne. Et la variable de retour n'é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

sortie:

# 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 () ne peut pas être appelé. Parce que ça ne fait aucun moyen.

Ne vous souciez pas de savoir si une variable est allouée sur un tas ou une pile, utilisez-la. Protégez-le par mutex ou par canal si nécessaire.

0
g10guang