Asignación de estructuras vs pila de estructuras en Go, y cómo se relacionan con la recolección de basura

165

Soy nuevo en Go y estoy experimentando un poco de disonancia congitiva entre la programación basada en pila de 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 pila de estilo Python donde Lo único que vive en la pila son referencias / punteros a objetos en el montón.

Por lo que puedo decir, las dos funciones siguientes dan la misma salida:

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 habría 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 para cuando la función hubiera regresado, lo que sería una mala cosa.

Si lo hubiera escrito en Python (o en muchos otros lenguajes modernos, excepto C #), el ejemplo 2 no hubiera sido posible.

Entiendo que Go basura recolecta ambos valores, por lo que las dos formas anteriores 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 vuelve la función. De hecho, tomar la dirección de un literal compuesto asigna una nueva instancia cada vez que se evalúa, por lo que podemos combinar estas dos últimas líneas.

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

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? ¿Se declara eso en la pila de la misma manera que lo estaría en C o también va en el montón?

2 - Si el ejemplo 2 se declara en la pila, ¿cómo permanece disponible después de que la función regrese?

3 - Si el ejemplo 2 se declara realmente en el montón, ¿cómo es que las estructuras se pasan por valor en lugar de por referencia? ¿Qué sentido tienen los punteros en este caso?

Joe
fuente

Respuestas:

170

Vale la pena señalar que las palabras "apilar" y "montón" no aparecen en ninguna parte de la especificación del idioma. Su pregunta está redactada con "... se declara en la pila" y "... se declara en el montón", pero tenga en cuenta que la sintaxis de declaración Go no dice nada sobre la pila o el montón.

Eso técnicamente hace que la respuesta a la implementación de todas sus preguntas dependa. 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 " newsiempre 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. Sin embargo, creo que la implementación actual de Go sigue una regla rígida en este caso, que es que si se toma la dirección de cualquier parte de una estructura, la estructura se va al montón.

Para la pregunta 3, corremos el riesgo de confundirnos con la terminología. Todo en Go se 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 ensamblaje 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 porque la salida de myFunction1 aquí es diferente que en la respuesta (excelente) de peterSO. Obviamente estamos ejecutando diferentes compiladores. 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. Sin embargo, 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 llamada a runtime.new, y sí, realmente funciona para devolver un objeto de 8 MB por valor. Funciona, pero generalmente no querrías hacerlo. El objetivo de un puntero aquí sería evitar empujar alrededor de objetos de 8MB.

Sonia
fuente
9
Excelente, gracias. Realmente no estaba preguntando "cuál es el punto de los punteros en absoluto", era más como "cuál es el punto de los punteros cuando los valores parecen comportarse como punteros", y ese caso es discutible por su respuesta de todos modos.
Joe
25
Se agradecería una breve explicación de la asamblea.
ElegEnt
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
}

En ambos casos, las implementaciones actuales de Go asignarían memoria para un structtipo MyStructTypeen un montón y devolverían su dirección. Las funciones son equivalentes; la fuente del compilador asm 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     ,

Llamadas

En una llamada a función, el valor de la función y los argumentos se evalúan en el orden habitual. Después de que se evalúan, los parámetros de la llamada se 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 devuelven por valor a la función de llamada cuando la función vuelve.

Todos los parámetros de función y retorno se pasan por valor. El valor del parámetro de retorno con tipo *MyStructTypees una dirección.

PeterSO
fuente
¡Muchas gracias! Voté a favor, pero acepto el de Sonia por la parte del análisis de escape.
Joe
1
Peter: Entonces, ¿cómo están tú y @Sonia produciendo esa asamblea? Ambos tienen el mismo formato. No puedo producirlo independientemente del comando / flags, después de haber probado objdump, go tool, otool.
10 cls
3
Ah, lo tengo - gcflags.
10 cls 10 de
30

De acuerdo con las preguntas frecuentes de Go :

si el compilador no puede probar que no se hace referencia a la variable después de que la función regrese, entonces el compilador debe asignar la variable en el montón de basura recolectada para evitar errores de puntero.

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

    ...

    return chunk, nil
}


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

    ...

    return &chunk, nil
}

Function1 y Function2 pueden ser funciones en línea. Y la variable de retorno no escapará. No es necesario asignar variables en el montón.

Mi código de ejemplo:

 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  }

Según la salida de cmd:

go run -gcflags -m test.go

salida:

# 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 el compilador es lo suficientemente inteligente, F1 () F2 () F3 () puede no ser llamado. Porque no tiene medios.

No importa si una variable está asignada en el montón o en la pila, solo utilícela. Protéjalo con mutex o canal si es necesario.

g10guang
fuente