La esencia de la entrevista en lenguaje GO: ¿cómo se realiza el análisis de escape?

En los principios de compilación, el método de analizar el rango dinámico de los punteros se denomina análisis de escape. En términos sencillos, cuando varios métodos o subprocesos hacen referencia a un puntero a un objeto, decimos que el puntero se ha escapado.

El análisis de escape en el lenguaje Go es una optimización y simplificación de la gestión de la memoria después de que el compilador realiza un análisis de código estático y puede determinar si una variable está asignada en el montón o en la pila.

Todos los estudiantes que han escrito C/C++ saben que llamar al famoso malloc y nuevas funciones puede asignar una parte de memoria en el montón. La responsabilidad del uso y destrucción de esta memoria recae en el programador. Si no tiene cuidado, se producirá una pérdida de memoria.

En el lenguaje Go, básicamente no hay necesidad de preocuparse por las pérdidas de memoria. Aunque también hay una nueva función, la memoria obtenida mediante el uso de la nueva función no está necesariamente en el montón. La diferencia entre el montón y la pila es "borrosa" para los programadores. Por supuesto, todo esto lo hace el compilador de Go detrás de escena.

El principio más básico del análisis de escape en el lenguaje Go es: si una función devuelve una referencia a una variable, escapará.

En pocas palabras, el compilador analizará las características del código y el ciclo de vida del código. Las variables en Go solo se asignarán a la pila si el compilador puede demostrar que no se hará referencia a ellas nuevamente después de que la función regrese. En otros casos , se asignarán a la pila.

No existe ninguna palabra clave o función en el lenguaje Go que pueda hacer que el compilador asigne directamente variables en el montón, sino que el compilador determina dónde asignar las variables analizando el código.

Tomando la dirección de una variable se puede asignar en el montón. Sin embargo, después de que el compilador realiza el análisis de escape, si se descubre que no se hará referencia a esta variable después de que regrese la función, aún se asignará en la pila.

El compilador decidirá si escapar en función de si se hace referencia a la variable externamente:

  1. Si no hay ninguna referencia fuera de la función, se colocará primero en la pila;
  2. Si hay una referencia fuera de la función, se debe colocar en el montón;

Al escribir código C/C++, para mejorar la eficiencia, el paso por valor (paso por valor) a menudo se "actualiza" al paso por referencia en un intento de evitar la ejecución del constructor y devolver directamente un puntero.

Aún debes recordar que hay un gran problema escondido aquí: se define una variable local dentro de la función y luego se devuelve la dirección (puntero) de esta variable local. Estas variables locales se asignan en la pila (asignación de memoria estática). Una vez que se ejecuta la función, la memoria ocupada por las variables será destruida. Cualquier acción sobre este valor de retorno (como la desreferenciación) interrumpirá la ejecución del programa y Incluso provocar que el programa se bloquee directamente. Por ejemplo, el siguiente código:

int *foo ( void )   
{
    
       
    int t = 3;
    return &t;
}

Algunos estudiantes pueden ser conscientes del error anterior y utilizar un enfoque más inteligente: utilizar la nueva función dentro de la función para construir una variable (asignación de memoria dinámica) y luego devolver la dirección de esta variable. Debido a que las variables se crean en el montón, no se destruyen cuando sale la función. ¿Pero es esto suficiente? ¿Cuándo y dónde se debe eliminar el objeto creado por new? La persona que llama puede olvidarse de eliminar o pasar directamente el valor de retorno a otras funciones, y luego ya no se puede eliminar, lo que significa que se produce una pérdida de memoria. Con respecto a este error, puede leer la Cláusula 21 de "C ++ efectivo", ¡que es muy buena!

C ++ es reconocido como el lenguaje con la sintaxis más compleja y se dice que nadie puede dominar completamente la sintaxis de C ++. Todo esto es muy diferente en el lenguaje Go. Coloque el código C++ como en el ejemplo anterior en Go sin ningún problema.

Los problemas que surgen en C/C++ mencionados anteriormente se promueven fuertemente como una característica del lenguaje en Go. ¡Es realmente el arsénico de C/C++ y la miel de Go!

La memoria asignada dinámicamente en C/C++ requiere que la liberemos manualmente, lo que nos hace caminar sobre hielo fino al escribir programas. Esto tiene sus ventajas: el programador tiene control total sobre la memoria. Pero también hay muchas deficiencias: a menudo se olvida liberar memoria, lo que provoca pérdidas de memoria. Por lo tanto, muchos lenguajes modernos han agregado mecanismos de recolección de basura.

La recolección de basura de Go hace que el montón y la pila sean transparentes para los programadores. Realmente libera las manos de los programadores, permitiéndoles concentrarse en los negocios y completar la escritura de código de manera "eficiente". Deje esos complejos mecanismos de administración de memoria en manos del compilador y los programadores podrán disfrutar de sus vidas.

La "operación descarada" del análisis de escape asigna racionalmente las variables a donde deben ir. Incluso si solicita memoria con nueva, si encuentro que ya no es útil después de salir de la función, lo arrojaré a la pila. Después de todo, la asignación de memoria en la pila es mucho más rápida que en el montón; incluso si parece que tiene Es solo una variable ordinaria, pero después del análisis de escape se descubre que se hace referencia a ella en otro lugar después de salir de la función, entonces la asignaré al montón.

Si se asignan variables en el montón, el montón no se puede limpiar automáticamente como la pila. Hará que Go realice una recolección de basura con frecuencia, y la recolección de basura ocupará una sobrecarga del sistema relativamente grande (ocupando el 25% de la capacidad de la CPU).

En comparación con la pila, el montón es adecuado para la asignación de memoria de tamaños impredecibles. Pero el precio que se paga por esto son asignaciones más lentas y fragmentación de la memoria. La asignación de memoria de pila será muy rápida. La memoria de asignación de pila solo requiere dos instrucciones de CPU: "PUSH" y "RELEASE" para asignar y liberar, mientras que la memoria de asignación de montón primero necesita encontrar un bloque de memoria del tamaño apropiado y luego debe liberarse mediante la recolección de basura.

A través del análisis de escape, puede intentar asignar variables que no necesitan asignarse en el montón directamente a la pila. Menos variables en el montón reducirán el costo de asignar memoria del montón, al mismo tiempo que reducirán la presión sobre el gc y mejorarán la Velocidad de ejecución del programa.

Extensión 1: ¿Cómo comprobar si una variable se ha escapado?
Dos métodos: usar el comando go para ver los resultados del análisis de escape, desmontar el código fuente;

Por ejemplo, use este ejemplo:

package main

import "fmt"

func foo() *int {
	t := 3
	return &t;
}

func main() {
	x := foo()
	fmt.Println(*x)
}

Utilice el comando ir:

go build -gcflags '-m -l' main.go

Se agregó -lpara evitar que la función foo esté incorporada. Obtenga el siguiente resultado:

# command-line-arguments
src/main.go:7:9: &t escapes to heap
src/main.go:6:7: moved to heap: t
src/main.go:12:14: *x escapes to heap
src/main.go:12:13: main ... argument does not escape

Las variables en la función foo tescaparon, tal como esperábamos. Lo que nos desconcierta es por qué la función principal xtambién se escapa. Esto se debe a que algunos parámetros de función son de tipo interfaz, como fmt.Println(a...interface{}), es difícil determinar el tipo específico de sus parámetros durante la compilación y también se producirán escapes.

Código fuente de desmontaje:

go tool compile -S main.go

Al interceptar parte de los resultados, las instrucciones marcadas en la figura indican tque se asignó memoria en el montón y se produjo un escape.
Insertar descripción de la imagen aquí

Extensión 2: ¿Se han escapado las variables del siguiente código?
Ejemplo 1:

package main
type S struct {}

func main() {
  var x S
  _ = identity(x)
}

func identity(x S) S {
  return x
}

Análisis: las funciones del lenguaje Go se pasan por valor. Al llamar a una función, se copia directamente una copia de los parámetros en la pila y no hay escape.

Ejemplo 2:

package main

type S struct {}

func main() {
  var x S
  y := &x
  _ = *identity(y)
}

func identity(z *S) *S {
  return z
}

Análisis: la entrada de la función de identidad se considera directamente como el valor de retorno, debido a que no hay referencia a z, z no escapa. La referencia a x no ha escapado del alcance de la función principal, por lo que x no ha escapado.

Ejemplo 3:

package main

type S struct {}

func main() {
  var x S
  _ = *ref(x)
}

func ref(z S) *S {
  return &z
}

Análisis: z es una copia de x. Se toma una referencia a z en la función de referencia, por lo que z no se puede colocar en la pila. De lo contrario, ¿cómo se puede encontrar z por referencia fuera de la función de referencia, por lo que z debe escapar al montón? . Aunque en la función principal el resultado de ref se descarta directamente, el compilador de Go no es tan inteligente y no puede analizar esta situación. Nunca hay una referencia a x, por lo que x no escapará.

Ejemplo 4: ¿Qué pasa si asigna una referencia a un miembro de la estructura?

package main

type S struct {
  M *int
}

func main() {
  var i int
  refStruct(i)
}

func refStruct(y int) (z S) {
  z.M = &y
  return z
}

Análisis: la función refStruct toma una referencia a y, por lo que y escapa.

Ejemplo 5:

package main

type S struct {
  M *int
}

func main() {
  var i int
  refStruct(&i)
}

func refStruct(y *int) (z S) {
  z.M = y
  return z
}

Análisis: se toma una referencia a i en la función principal y se pasa a la función refStruct. La referencia de i siempre se usa en el alcance de la función principal, por lo que i no escapa. En comparación con el ejemplo anterior, hay una pequeña diferencia, pero el efecto del programa resultante es diferente: en el Ejemplo 4, primero se asigna i en el marco de la pila principal, luego se asigna en el marco de la pila refStruct y luego se escapa al montón. Asignado una vez en el montón, un total de 3 asignaciones. En este ejemplo, i se asigna solo una vez y luego se pasa por referencia.

Ejemplo 6:

package main

type S struct {
  M *int
}

func main() {
  var x S
  var i int
  ref(&i, &x)
}

func ref(y *int, z *S) {
  z.M = y
}

Análisis: En este ejemplo, he escapado, según el análisis del ejemplo anterior 5, no escaparé. La diferencia entre los dos ejemplos es que S en el ejemplo 5 está en el valor de retorno y la entrada solo puede "fluir" hacia la salida. En este ejemplo, S está en el parámetro de entrada, por lo que el análisis de escape falla y necesito escapar al montón.

Supongo que te gusta

Origin blog.csdn.net/zy_dreamer/article/details/132795412
Recomendado
Clasificación