util - Asignación de estructuras de pila y de montón en Go, y cómo se relacionan con la recolección de basura
que se hace con las pilas que ya no sirven (4)
Soy nuevo en Go y estoy experimentando una cierta disonancia congitiva entre la programación basada en la pila estilo C donde las variables automáticas viven en la pila y la memoria asignada vive en el montón y la programación basada en la pila al estilo de Python donde el Lo único que vive en la pila son las referencias / punteros a los objetos en el montón.
Hasta donde puedo decir, las dos funciones siguientes dan el mismo resultado:
func myFunction() (*MyStructType, error) {
var chunk *MyStructType = new(HeaderChunk)
...
return chunk, nil
}
func myFunction() (*MyStructType, error) {
var chunk MyStructType
...
return &chunk, nil
}
es decir, asignar una nueva estructura y devolverla.
Si hubiera escrito eso en C, el primero habría puesto un objeto en el montón y el segundo lo hubiera puesto en la pila. El primero devolvería un puntero al montón, el segundo devolvería un puntero a la pila, que se habría evaporado cuando la función regresara, lo que sería una cosa mala.
Si lo hubiera escrito en Python (o en muchos otros idiomas modernos, excepto C #), el ejemplo 2 no hubiera sido posible.
Veo que Go basura recoge ambos valores, por lo que ambos formularios están bien.
Citar:
Tenga en cuenta que, a diferencia de C, está perfectamente bien devolver la dirección de una variable local; el almacenamiento asociado con la variable sobrevive después de que la función retorna. De hecho, tomar la dirección de un literal compuesto asigna una instancia nueva cada vez que se evalúa, por lo que podemos combinar estas dos últimas líneas.
Pero plantea un par de preguntas.
1 - En el ejemplo 1, la estructura se declara en el montón. ¿Qué pasa con el ejemplo 2? ¿Eso está declarado en la pila de la misma manera que sería en C o también está en el montón?
2 - Si el ejemplo 2 se declara en la pila, ¿cómo queda disponible después de que la función retorna?
3 - Si el ejemplo 2 realmente se declara en el montón, ¿cómo es que las estructuras se pasan por valor en lugar de por referencia? ¿Cuál es el punto de los indicadores en este caso?
No siempre se sabe si su variable está asignada en la pila o en el montón.
...
Si necesita saber dónde se asignan sus variables, pase el indicador "-m" gc para "ir a compilar" o "ir a ejecutar" (por ejemplo,go run -gcflags -m app.go
).
Fuente: http://devs.cloudimmunity.com/gotchas-and-common-mistakes-in-go-golang/index.html#stack_heap_vars
De acuerdo con las preguntas frecuentes de Go :
si el compilador no puede probar que la variable no se referencia después de que la función retorna, entonces el compilador debe asignar la variable en el montón recogido de basura para evitar errores de puntero colgando.
Vale la pena señalar que las palabras "apilar" y "apilar" no aparecen en ninguna parte de la especificación del idioma. Su pregunta está redactada con "... está declarado en la pila" y "... declarado en el montón", pero tenga en cuenta que la sintaxis de la declaración Go no dice nada sobre la pila o el montón.
Eso técnicamente hace que la respuesta a todas sus preguntas dependa de la implementación. En realidad, por supuesto, hay una pila (por goroutine!) Y un montón, y algunas cosas van en la pila y otras en el montón. En algunos casos, el compilador sigue reglas rígidas (como " new
siempre asigna en el montón") y en otros el compilador hace "análisis de escape" para decidir si un objeto puede vivir en la pila o si debe asignarse en el montón.
En su ejemplo 2, el análisis de escape mostraría el puntero a la estructura de escape y, por lo tanto, el compilador tendría que asignar la estructura. Creo que la implementación actual de Go sigue una regla rígida en este caso, que es que si la dirección se toma de cualquier parte de una estructura, la estructura continúa en el montón.
Para la pregunta 3, corremos el riesgo de confundirnos con la terminología. Todo en Go pasa por valor, no hay pase por referencia. Aquí está devolviendo un valor de puntero. ¿Cuál es el punto de los punteros? Considere la siguiente modificación de su ejemplo:
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
}
Modifiqué myFunction2 para devolver la estructura en lugar de la dirección de la estructura. Compare la salida de conjunto de myFunction1 y myFunction2 ahora,
--- 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 ,
No se preocupe, la salida de myFunction1 aquí es diferente a la respuesta de peterSO (excelente). Obviamente estamos ejecutando compiladores diferentes. De lo contrario, vea que modifiqué myFunction2 para devolver myStructType en lugar de * myStructType. La llamada a runtime.new se ha ido, lo que en algunos casos sería algo bueno. Espera, aquí está 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 ,
Todavía no hay llamadas a runtime.new, y sí, realmente funciona para devolver un objeto de 8MB por valor. Funciona, pero normalmente no querrías. El punto de un puntero aquí sería evitar empujar objetos de 8MB.
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
}
En ambos casos, las implementaciones actuales de Go asignarían memoria a una struct
de tipo MyStructType
en un montón y devolverían su dirección. Las funciones son equivalentes; la fuente del asm del compilador es la misma.
--- 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 ,
En una llamada a función, el valor y los argumentos de la función se evalúan en el orden habitual. Después de que se evalúan, los parámetros de la llamada pasan por valor a la función y la función llamada comienza a ejecutarse. Los parámetros de retorno de la función se pasan por valor de regreso a la función de llamada cuando la función retorna.
Todos los parámetros de función y retorno se pasan por valor. El valor del parámetro de retorno con tipo *MyStructType
es una dirección.