Explicación detallada del principio, proceso y código fuente del mecanismo de Lua GC

La recolección de basura de Lua (Garbage Collect) es una parte más importante de lua. Debido a los cambios en la versión del código fuente de Lua, la mayoría de los artículos actuales sobre este tema todavía se basan en la versión Lua5.1, que tiene un cierto retraso. Por lo tanto, al referirse al código fuente actual de Lua de la versión 5.3.4, este artículo espera tener una discusión más detallada sobre el algoritmo GC de Lua.

1. Breve descripción del principio del algoritmo de recolección de basura Lua

Lua adopta el algoritmo Mark and Sweep GC. El algoritmo se describe brevemente:
Marcar : cada vez que se ejecuta GC, comience con un número de nodos raíz y marque los nodos directa o indirectamente relacionados con ellos uno por uno;
claro : Cuando se complete el marcado, recorra toda la lista de objetos vinculados y elimine los nodos marcados como que deben eliminarse uno por uno.

2. Tres colores en la recolección de basura Lua

El llamado color es la marca mencionada en el "Resumen de algoritmo" anterior. Lua usa blanco, gris y negro para marcar el estado reciclable de un objeto. (El blanco se divide en blanco 1, blanco 2)
Blanco : estado reciclable.
详解: Si el objeto no ha sido marcado por GC, entonces el blanco representa que el objeto actual está en estado de espera. Ejemplo: el estado inicial de un objeto recién creado debe establecerse en blanco, porque el objeto no ha sido marcado por el GC, por lo que el color del estado inicial permanece sin cambios, todavía blanco. Si el objeto sigue siendo blanco después de que finaliza la fase de marcado de GC, entonces el blanco representa que el objeto actual es reciclable. Pero, de hecho, el ajuste blanco es esencialmente para marcar la reciclabilidad.
Gris : estado intermedio.
详解: Se marcará el objeto actual. Ejemplo: el objeto actual ha sido visitado por el GC, pero otros objetos a los que hace referencia este objeto no se han marcado.
Negro : estado no reciclable.
详解: El objeto actual está en el estado marcado. Ejemplo: el objeto actual ha sido visitado por el GC y otros objetos a los que hace referencia el objeto también están marcados.
备注: El blanco se divide en blanco 1 y blanco 2. Motivo: Al final de la fase de marcado de GC y la fase de limpieza aún no ha comenzado, si se crea un nuevo objeto, se debe marcar como blanco en principio porque no se ha encontrado referenciado, por lo que la fase de limpieza posterior se creará de acuerdo con las reglas de limpieza del blanco. El objeto está despejado. Esto es irrazonable. Entonces lua usa dos tipos de blanco para marcar. Si ocurre la situación anterior, lua marcará el objeto recién creado como blanco, pero es "blanco actual" (como el blanco 1). Lua solo limpiará el "blanco antiguo" (como el blanco 2) durante la fase de limpieza, una vez finalizada la limpieza, actualizará el "blanco actual", es decir, el blanco 2 como el blanco actual. La siguiente ronda de GC limpiará el objeto de identificación blanco 1 como el "blanco viejo". Resuelva los problemas mencionados anteriormente mediante esta técnica. Como se muestra en la figura siguiente: (En la figura siguiente, para facilitar la comprensión de la conversión de color, no se considera la influencia de la barrera)
Tabla de conversión de color2018-12-26-Color_conversion_chart

3. Proceso detallado de recolección de basura de Lua

Algoritmo flow1 / 2018-12-26-Algoritmo_flujo1
Flujo del algoritmo 2 / 2018-12-26-Algoritm_flow2

4. Explicación detallada del código fuente del paso

4.1 Fase de nuevo objeto

PASO 1: Crea un nuevo objeto reciclable y marca su color como blanco.
Explicación: Primero, Lua abstrae el tipo de datos. La estructura de datos específica se puede ver en el siguiente código fuente:
(lobject.h)

** Common type for all collectable objects
*/
typedef struct GCObject GCObject;

/*
** Common Header for all collectable objects (in macro form, to be
** included in other objects)
*/
#define CommonHeader  GCObject *next; lu_byte tt; lu_byte marked

/*
** Common type has only the common header
*/
struct GCObject {
    
    
  CommonHeader;
};


/*
** Tagged Values. This is the basic representation of values in Lua,
** an actual value plus a tag with its type.
*/
/*
** Union of all Lua values
*/
typedef union Value {
    
    
  GCObject *gc;    /* collectable objects */
  void *p;         /* light userdata */
  int b;           /* booleans */
  lua_CFunction f; /* light C functions */
  lua_Integer i;   /* integer numbers */
  lua_Number n;    /* float numbers */
} Value;

#define TValuefields  Value value_; int tt_

typedef struct lua_TValue {
    
    
  TValuefields;
} TValue;

12345678910111213141516171819202122232425262728293031323334353637383940

A partir del análisis de código, podemos ver que en la implementación de Lua, los tipos de datos se pueden dividir en objetos que deben ser recuperados por la administración de GC y objetos almacenados por valor. Y STEP1 mencionó " objetos reciclables " (GCObject) se refiere a objetos que necesitan ser administrados GC recuperados en lua5.3, específicamente se refiere a: TString, Udata, Cloure, Table , Proto, lua_State ( al ver GCUnion) Estos tipos de datos. Estos tipos de datos tienen una parte común CommonHeader:
(lobject.h)

#define CommonHeader
GCObject *next; lu_byte tt; lu_byte marked
12

Entre ellos, los siguientes enlaces al siguiente GCObject, tt indica el tipo de datos y marcado se usa para almacenar el color mencionado anteriormente.
La creación del objeto de tipo de datos anterior se logra llamando a la función de creación de tipo de datos correspondiente por la máquina virtual lua. Durante el proceso de creación, la función (lgc.c) luaC_newobj siempre se llama para completar la inicialización de GCObject.
Tomando Table como ejemplo, se llamará a su vez:
(lvm.c)[luaV_execute] -> (ltable.c)[luaH_new] -> (lgc.c)[luaC_newobj]
Veamos luaC_newobj:
(lgc.c)

/*
** create a new collectable object (with given type and size) and link
** it to 'allgc' list.
*/
GCObject *luaC_newobj (lua_State *L, int tt, size_t sz) {
    
    
  global_State *g = G(L);
  GCObject *o = cast(GCObject *, luaM_newobject(L, novariant(tt), sz));
  o->marked = luaC_white(g);
  o->tt = tt;
  o->next = g->allgc;
  g->allgc = o;
  return o;
}
12345678910111213

La función se completa en secuencia:

  • 1. Utilice luaM_newobject para asignar memoria y actualizar GCdebt (parámetros relacionados con el uso de memoria)
  • 2. Establezca GCObject en "blanco actual", establezca el tipo de datos y monte GCObject en la lista vinculada de asignación

注意: La lista vinculada de asignación es una estructura de datos importante en la que el GC atraviesa las marcas de objeto en la lista vinculada y las borra de acuerdo con las marcas.
Así que comenzamos nuestro viaje de GC creando un objeto reciclable.

4.2 Condiciones de activación

PASO 2:
Lua se divide en modos de GC automático y manual cuando se alcanzan las condiciones de GC .
Manual:
(lapi.h)

 LUAI_FUNC void luaC_step (lua_State *L);
1

Automático:
(lgc.h)

#define luaC_condGC(L,pre,pos) \
  { if (G(L)->GCdebt > 0) { pre; luaC_step(L); pos;}; \
    condchangemem(L,pre,pos); }
/* more often than not, 'pre'/'pos' are empty */
#define luaC_checkGC(L)   luaC_condGC(L,(void)0,(void)0)
12345

El método manual es iniciar el GC llamando a collectgarbage ([opt [, arg]]). El método automático debe cumplir con las condiciones del GC. Si revisamos lapi.c / ldo.c / lvm.c, encontraremos que la mayoría de las API que provocan el crecimiento de la memoria se han llamado luaC_checkGC. De esta manera, la GC se puede realizar automáticamente a medida que aumenta el uso de la memoria. Estas condiciones de activación se calculan mediante parámetros como g-> GCdebt y g-> totalbytes. Debido al significado de los parámetros, el método de cálculo de los parámetros y otras cuestiones deben ser más elaboradas. Debido a las limitaciones de espacio, voy a desarrollar los siguientes artículos sobre el ajuste manual del GC. Pero en la actualidad , las condiciones de activación de luaGC se pueden resumir: cuando la memoria utilizada por lua alcanza el umbral, GC se activará . Por supuesto, este umbral se establece de forma dinámica.

4.3 Máquina de estado de función de GC

Cuando se cumplen las condiciones de GC, se ingresa al proceso de GC real.
Antes de iniciar este proceso, es necesario elaborar una pregunta. El GC anterior a lua5.0 solo usa colores de identificación blanco y negro, por lo que el GC necesita detener el mundo, es decir, el proceso del GC debe completarse de una vez. Después de que lua5.1 adopta la identificación de tres colores, la mayor mejora es darse cuenta del paso a paso.
** La importancia de esta mejora es mejorar el rendimiento en tiempo real del sistema lua, ** para que el proceso de GC se pueda ejecutar en etapas. Esta mejora se debe a una serie de cambios, como la introducción del gris, la introducción del mecanismo de barrera y el desglose del estado de la función de un solo paso (la función más importante en el proceso de GC). Pero creo que el principio de realizar paso a paso es la introducción del gris. Esto se debe a que si solo hay dos colores de blanco y negro, el estado de cada objeto es solo "dual" y no puede tener un estado intermedio, por lo que la operación del GC no debe interrumpirse.
A continuación, analice singletep, la función más importante del mecanismo luaGC. Todos los procesos de GC comienzan con la función de un solo paso.
(lgc.h)

static lu_mem singlestep (lua_State *L)
1

** singletep es en realidad una máquina de estado simple. ** detalles de la siguiente manera:
Máquina de estado de GC / 2018-12-26-GC_StateMachine

4.4 Fase de marcado

PASO 3: Comience a marcar desde el objeto raíz, cambie de blanco a gris y agréguelo a la lista enlazada gris.
El estado correspondiente real de este paso: GCSpause
(lgc.c)[singlestep]

switch (g->gcstate) {
    
    
      case GCSpause: {
    
    
      g->GCmemtrav = g->strt.size * sizeof(GCObject*);
      restartcollection(g);
      g->gcstate = GCSpropagate;
      return g->GCmemtrav;
      }
1234567

El trabajo principal se completa con la función restartcollection:
(lgc.c)

/*
** mark root set and reset all gray lists, to start a new collection
*/
static void restartcollection (global_State *g) {
    
    
  g->gray = g->grayagain = NULL;
  g->weak = g->allweak = g->ephemeron = NULL;
  markobject(g, g->mainthread);
  markvalue(g, &g->l_registry);
  markmt(g);
  markbeingfnz(g);  /* mark any finalizing object left from previous cycle */
}
1234567891011
  • 1. Inicialice y borre las listas enlazadas de varios tipos de objetos utilizados para el marcado auxiliar, donde g-> gray es la cadena de nodos gris; g-> grayagain es la cadena de nodos gris que requiere marcas de operación atómica; g-> débil, g-> allweak, g-> ephemeron es una cadena relacionada con tablas débiles.
  • 2. Luego use markobject, markvalue, markmt, markbeingfnz para marcar los objetos raíz (globales) a su vez: mainthread (hilo principal (coroutine), registro (registro), metatabla global (metatabla), en la finalización restante en el último ciclo de GC Y agréguelo a la cadena de etiquetas auxiliar correspondiente.

PASO 4: ¿Está vacía la lista enlazada gris?
PASO 5: Saque un objeto de la lista enlazada gris, márquelo como negro y recorra otros objetos asociados con este objeto.
El estado correspondiente de los pasos anteriores: GCSpropagate , que es el contenido principal de la fase de marcado.
(lgc.c)[singlestep]

case GCSpropagate: {
    
    
      g->GCmemtrav = 0;
      lua_assert(g->gray);
      propagatemark(g);
      if (g->gray == NULL)  /* no more gray objects? */
       g->gcstate = GCSatomic;  /* finish propagate phase */
      return g->GCmemtrav;  /* memory traversed in this step */
  }
12345678

El trabajo principal se completa con la función propagatemark:
(lgc.c)

/*
** traverse one gray object, turning it to black (except for threads,
** which are always gray).
*/
static void propagatemark (global_State *g) {
    
    
  lu_mem size;
  GCObject *o = g->gray;
  lua_assert(isgray(o));
  gray2black(o);
  switch (o->tt) {
    
    
    case LUA_TTABLE: {
    
    
      Table *h = gco2t(o);
      g->gray = h->gclist;  /* remove from 'gray' list */
      size = traversetable(g, h);
      break;default: lua_assert(0); return;
  }
  g->GCmemtrav += size;
}
1234567891011121314151617181920

En el paso 4, el ciclo de juzgar la lista enlazada gris en realidad no se realiza a través del ciclo. Pero si la tabla gris no está vacía, el estado no cambiará. Además, cada vez que se entra en la máquina de estado, dado que el estado no ha cambiado, la función de procesamiento correspondiente a este estado se ejecuta repetidamente. Hasta que el estado cambie, ingrese al siguiente estado.
El motivo del diseño se puede entender junto con la función propagetemark. En propagatemark, solo se tomará un nodo gris de la lista enlazada gris a la vez, se establecerá en negro (diferente del GC de lua5.1) y se eliminará de la lista enlazada gris. Atraviese otros nodos relacionados con este nodo y agregue los nodos relacionados a la cadena gris. Hasta ahora, el procesamiento de estado de GCSpropagate se ha completado. No es difícil encontrar que este proceso solo procesó un nodo gris en la lista gris enlazada original. Esto se debe a que el marcado de los nodos relacionados con el nodo correspondiente se realiza en realidad a través de un recorrido, este proceso será muy costoso, por lo que Lua solo espera procesar uno de esos nodos cada vez que se propague GCS.
La ventaja de esto es que los costosos pasos se llaman varias veces, lo que reduce el tiempo de cada bloque. Al mismo tiempo, trae un nuevo problema: si la velocidad de creación de objetos asignados de Lua es mucho más rápida que la velocidad de GCSpropagate, entonces el proceso GC de Lua se bloqueará en el estado de GCSpropagate. 解决方法就留给读者思考了.
En el método propagetemark, las operaciones de clasificación se realizan para diferentes tipos de datos, pero eventualmente terminarán en el método de reallymarkobject:
(lgc.c)

static void reallymarkobject (global_State *g, GCObject *o) {
    
    
 reentry:
  white2gray(o);
  switch (o->tt) {
    
    
    case LUA_TSHRSTR: {
    
    
      gray2black(o);
      g->GCmemtrav += sizelstring(gco2ts(o)->shrlen);
      break;
    }case LUA_TTABLE: {
    
    
      linkgclist(gco2t(o), g->gray);
      break;
    }default: lua_assert(0); break;
  }
}
123456789101112131415161718

Si es un tipo de cuerda, por su particularidad, se puede juzgar directamente como negro. Otros tipos se unen a la cadena gris u otras cadenas auxiliares de marcado.
PASO 6: Finalmente, borre la lista enlazada gris una vez y asegúrese de que sea una operación atómica.
Estado correspondiente real de este paso: GCSatomic
(lgc.c)[singlestep]

case GCSatomic: {
    
    
      lu_mem work;
      propagateall(g);  /* make sure gray list is empty */
      work = atomic(L);  /* work is what was traversed by 'atomic' */
      entersweep(L);
      g->GCestimate = gettotalbytes(g);  /* first estimate */;
      return work;
    }
12345678

1.Propagateall
(lgc.c)

static void propagateall (global_State *g) {
    
    
      while (g->gray) propagatemark(g);}
12

Primero marque g-> gray, debido a la existencia de la función luaC_barrier (un mecanismo para manejar objetos recién creados), puede manipular la variable g-> gray al llamar a reallymarkobject.
2. Atomic completa los 原子操作pasos requeridos , principalmente como sigue:

  • 1 Vuelva a recorrer (seguir) el objeto raíz.
  • 2 Atraviese el gris anterior de nuevo (habrá una mesa débil en gris nuevamente) y limpie el espacio de la mesa débil.
  • 3 Llame a la función separadatobefnz para colocar los objetos (blancos) que necesitan reciclarse con la función __gc en la tabla global_State.tobefnz y guárdelos para limpiarlos más tarde.
  • 4. Haga que todos los objetos en global_State.tobefnz sean accesibles.
  • 5. Cambie el valor de blanco actual a una nueva ronda de valor de blanco.

El contenido anterior es solo una breve descripción de atómico, hay muchos detalles específicos que involucrarán demasiado contenido, y no lo expandiré específicamente debido a limitaciones de espacio.

4.5 Fase de limpieza

PASO 7: Reciclar paso a paso según diferentes tipos de objetos. Recorra las listas vinculadas de almacenamiento de diferentes tipos de objetos en reciclaje
PASO8: Si la lista vinculada de almacenamiento de objetos llega al final de la cadena
PASO9: Determine si el color del objeto es blanco uno por uno
PASO10: Libere el espacio ocupado por
el objeto PASO 11: Establezca el color del objeto en blanco
El estado real correspondiente de los pasos anteriores: GCSswpallgc, GCSswpfinobj, GCSswptobefnz,GCSswpend
(lgc.c)[singlestep]

case GCSswpallgc: {
    
      /* sweep "regular" objects */
      return sweepstep(L, g, GCSswpfinobj, &g->finobj);
    }
  case GCSswpfinobj: {
    
      /* sweep objects with finalizers */
      return sweepstep(L, g, GCSswptobefnz, &g->tobefnz);
    }
  case GCSswptobefnz: {
    
      /* sweep objects to be finalized */
      return sweepstep(L, g, GCSswpend, NULL);
    }
  case GCSswpend: {
    
      /* finish sweeps */
      makewhite(g, g->mainthread);  /* sweep main thread */
      checkSizes(L, g);
      g->gcstate = GCScallfin;
      return 0;
}
123456789101112131415

Este proceso puede entenderse como clasificación y reciclaje de basura :
GCSswpallgc liberará todos los objetos muertos en g-> allgc a través de sweepstep (objetos con el GCSatomicvalor blanco anterior), y señalará los objetos vivos como el valor blanco actual.
Los dos estados de GCSswpfinobj y GCSswptobefnz también llaman a la función sweepstep. Pero las listas enlazadas g-> finobj y g-> tobefnz no pueden tener objetos muertos ( 原因留给读者思考), por lo que su función es solo restablecer estos objetos a una nueva ronda de blanco.
GCSswpend se usa para liberar algo de espacio en mainthread, como tabla de cadenas, búfer de conexión, etc.
El cambio de estos estados se logra determinando si el tipo actual de lista enlazada de objetos ha llegado al final.
La operación de limpieza real del estado anterior se completa llamando a sweeplist a través de sweepstep.
(lgc.c)

static GCObject **sweeplist (lua_State *L, GCObject **p, lu_mem count) {
    
    
  global_State *g = G(L);
  int ow = otherwhite(g);
  int white = luaC_white(g);  /* current white */
  while (*p != NULL && count-- > 0) {
    
    
    GCObject *curr = *p;
    int marked = curr->marked;
    if (isdeadm(ow, marked)) {
    
      /* is 'curr' dead? */
      *p = curr->next;  /* remove 'curr' from list */
      freeobj(L, curr);  /* erase 'curr' */
    }
    else {
    
      /* change mark to 'white' */
      curr->marked = cast_byte((marked & maskcolors) | white);
      p = &curr->next;  /* go to next element */
    }
  }
  return (*p == NULL) ? NULL : p;
}
123456789101112131415161718

Las listas vinculadas de varios objetos de tipo de datos recuperables mencionados anteriormente son muy largas, por lo que la eliminación también se realiza en segmentos. Por ejemplo, el número de limpiezas se puede controlar mediante el parámetro de recuento en la lista de barrido. (La configuración de este valor y el proceso de transformación dinámica, espero explicar en el próximo artículo sobre el ajuste de parámetros de GC). A partir del proceso de limpieza anterior, se puede entender que el proceso GC de Lua no es extraíble, es decir, no se migran datos y no se organiza la memoria.
El último estado de GC: GCScallfin
(lgc.c)

case GCScallfin: {
    
      /* call remaining finalizers */
      if (g->tobefnz && g->gckind != KGC_EMERGENCY) {
    
    
        int n = runafewfinalizers(L);
        return (n * GCFINALIZECOST);
      }
      else {
    
      /* emergency mode or no more finalizers */
        g->gcstate = GCSpause;  /* finish collection */
        return 0;
      }
}
12345678910

En este estado, los objetos de la lista enlazada g-> tobefnz se eliminarán uno por uno, y luego se llamará a su función __gc y se colocarán en la lista enlazada g-> tobefnz, listos para reclamar oficialmente este objeto en el siguiente ciclo de GC.

5. Resumen

En este punto, de hecho, se ha descrito el algoritmo GC de Lua. Pero para hacer eco del contenido del resumen del algoritmo al principio del artículo después de explicar el código fuente, también es conveniente que los lectores tengan una comprensión macro. Así que quiero terminar la descripción de este algoritmo con una oración al final: (Por supuesto, la expresión es solo para la situación principal)
Lua usa la lista enlazada gris para usar secuencialmente el objeto realmarkobject para marcar los objetos en color, y luego recorrer la lista enlazada alloc y luego usar la lista de barrido a su vez. Objetos claros que necesitan ser reciclados.

Esta publicación de blog se reproduce en: https://blog.csdn.net/BigBrick/article/details/85317491

Supongo que te gusta

Origin blog.csdn.net/qq_43801020/article/details/108073978
Recomendado
Clasificación