Detailed explanation of the principle, process and source code of Lua GC mechanism

Lua garbage collection (Garbage Collect) is an important part of lua. Due to the changes in the source code version of Lua, most of the current articles on this subject are still based on the Lua5.1 version, which has a certain lag. Therefore, by referring to the current Lua source code of version 5.3.4, this article hopes to have a more detailed discussion on Lua's GC algorithm.

1. Brief description of the principle of Lua garbage collection algorithm

Lua uses the Mark and Sweep GC algorithm. The algorithm is briefly described:
Mark : Each time GC is executed, start with a number of root nodes, and mark the nodes directly or indirectly related to them one by one;
clear : When the marking is completed, traverse the entire object linked list, and delete the nodes marked as needing to be deleted one by one.

2. Three colors in Lua garbage collection

The so-called color is the mark mentioned in the "Algorithm Brief" above. Lua uses white, gray, and black to mark the recyclable state of an object. (White is divided into white 1, white 2)
White : recyclable state.
详解: If the object has not been marked by GC, then white represents the current object is in the waiting state. Example: The initial state of a newly created object should be set to white, because the object has not been marked by the GC, so the initial state color remains unchanged, still white. If the object is still white after the GC marking phase ends, then white represents the current object is recyclable. But in fact, the white setting is essentially to mark recyclability.
Gray : intermediate state.
详解: The current object is to be marked. Example: The current object has been visited by the GC, but other objects referenced by this object have not been marked.
Black : non-recyclable state.
详解: The current object is in the marked state. Example: The current object has been visited by the GC, and other objects referenced by the object are also marked.
备注: White is divided into white 1 and white 2. Reason: At the end of the GC marking phase and the cleanup phase has not yet begun, if a new object is created, it should be marked as white in principle because it has not been found to be referenced, so the subsequent cleanup phase will be created according to the rules of white being cleaned. The object is cleared. This is unreasonable. So lua uses two kinds of white to mark. If the above situation occurs, lua will still mark the newly created object as white, but it is "current white" (for example, white 1). Lua will only clean the "old white" (such as white 2) during the cleaning phase, and after the cleaning is completed, it will update the "current white", that is, white 2 as the current white. The next round of GC will clean up the white 1 identification object as the "old white". Solve the above-mentioned problems through such a technique. As shown in the figure below: (In the figure below, in order to facilitate the understanding of color conversion, the influence of barrier is not considered)
Color conversion chart2018-12-26-Color_conversion_chart

3. Lua garbage collection detailed process

Algorithm flow1/2018-12-26-algorithm_flow1
Algorithm flow 2/2018-12-26-algorithm_flow2

4. Detailed explanation of step source code

4.1 New object stage

STEP1: Create a new recyclable object and mark its color as white.
Explanation: First, Lua abstracts the data type. The specific data structure can be seen in the following source code:
(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

It can be seen from code analysis that in the implementation of Lua, data types can be divided into objects that need to be recovered by GC management and objects stored by value. And STEP1 mentioned " recyclable objects " (GCObject) refers to objects that need to be managed GC recovered in lua5.3, specifically refers to: TString, Udata, Cloure, Table , Proto, lua_State ( by viewing GCUnion) These data types. These data types have a common part CommonHeader:
(lobject.h)

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

Among them, next links to the next GCObject, tt indicates the data type, and marked is used to store the aforementioned color.
The creation of the above data type object is accomplished by calling the corresponding data type creation function by the lua virtual machine. During the creation process, the function (lgc.c)luaC_newobj is always called to complete the initialization of GCObject.
Taking Table as an example, it will be called sequentially:
(lvm.c)[luaV_execute]-> (ltable.c)[luaH_new]-> (lgc.c)[luaC_newobj]
Let’s look at 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

The function is completed in sequence:

  • 1. Use luaM_newobject to allocate memory and update GCdebt (parameters related to memory usage)
  • 2. Set GCObject to "current white", set the data type, and mount GCObject to the alloc linked list

注意: The alloc linked list is an important data structure in which the GC traverses the object marks on the linked list in turn and clears them according to the marks.
So we started our GC journey by creating a recyclable object.

4.2 Trigger conditions

STEP2:
Lua is divided into automatic and manual GC modes when GC conditions are reached .
Manual:
(lapi.h)

 LUAI_FUNC void luaC_step (lua_State *L);
1

Automatic:
(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

The manual method is to start GC by calling collectgarbage ([opt [, arg]]). The automatic method needs to meet the GC conditions. If we examine lapi.c/ldo.c/lvm.c, we will find that most of the APIs that cause memory growth call luaC_checkGC. In this way, GC can be performed automatically as memory usage increases. These trigger conditions are calculated by parameters such as g-> GCdebt and g-> totalbytes. Due to the meaning of the parameters, the calculation method of the parameters, and other issues need to be further elaborated. Due to space limitations, I am going to elaborate on the following articles on manual GC tuning. But at present , the trigger conditions of luaGC can be summarized: when the memory used by lua reaches the threshold, GC will be triggered . Of course, this threshold is dynamically set.

4.3 GC function state machine

When the GC conditions are met, the real GC process is entered.
Before starting this process, one question needs to be elaborated. The GC before lua5.0 only uses white and black identification colors, so the GC needs to stop the world, that is, the GC process needs to be completed at once. After lua5.1 adopts three-color identification, the biggest improvement is to realize the step by step.
** The significance of this improvement is to improve the real-time performance of the lua system, ** so that the GC process can be executed in stages. This improvement is caused by a series of changes, such as the introduction of gray, the introduction of the barrier mechanism, and the state breakdown of the singlestep function (the most important function in the GC process). But I think that the principle of realizing step by step is the introduction of gray. This is because if there are only two colors of black and white, the state of each object is only "dual" and cannot have an intermediate state, so the GC operation must not be interrupted.
Next, analyze singlestep, the most important function in the luaGC mechanism. All GC processes start from the singlestep function.
(lgc.h)

static lu_mem singlestep (lua_State *L)
1

**singlestep is actually a simple state machine. **details as follows:
GC state machine/2018-12-26-GC_StateMachine

4.4 Marking phase

STEP3: Mark from the root object, turn white to gray, and add it to the gray linked list.
The actual corresponding state of this step: 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

The main work is completed by the restartcollection function:
(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. Initialize and clear the linked lists of various types of objects used for auxiliary marking, where g->gray is the gray node chain; g->grayagain is the gray node chain that requires atomic operation marks; g->weak, g->allweak, g->ephemeron is a chain related to weak tables.
  • 2. Then use markobject, markvalue, markmt, markbeingfnz to mark the root (global) objects in turn: mainthread (main thread (coroutine), registry (registry), global metatable (metatable), in the finalize remaining in the last GC cycle And add it to the corresponding auxiliary tag chain.

STEP4: Is the gray linked list empty?
STEP5: Take out an object from the gray linked list, mark it as black, and traverse other objects associated with this object.
The corresponding state of the above steps: GCSpropagate , which is the core content of the marking phase.
(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

The main work is completed by the propagatemark function:
(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

In step 4, the cycle of judging the gray linked list is actually not realized through cycle. But if the gray table is not empty, the status will not change. Furthermore, every time the state machine is entered, since the state has not changed, the processing function corresponding to this state is repeatedly executed. Until the state changes, enter the next state.
The reason for the design can be understood in conjunction with the propagetemark function. In propagatemark, only one gray node will be taken from the gray linked list at a time, set to black (different from the GC of lua5.1), and removed from the gray linked list. Traverse other nodes related to this node, and add related nodes to the gray chain. So far, a GCSpropagate state process has been completed. It is not difficult to find that this process only processed a gray node in the original gray linked list. This is because marking the nodes related to the corresponding node is actually done through traversal, this process will be very expensive, so lua only hopes to process one such node every time GCSpropagate.
The advantage of this is that the expensive steps are called multiple times, reducing the time of each block. At the same time, it brings a new problem. If the speed of Lua's creation of allocated objects is much greater than the speed of GCSpropagate, then the GC process of Lua will be blocked in the state of GCSpropagate. 解决方法就留给读者思考了.
In the propagetemark method, classification operations are performed for different data types, but eventually they will end up in the method of 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

If it is a string type, because of its particularity, it can be directly judged as black. Other types join the gray chain or other auxiliary marking chains.
STEP6: Finally, clear the gray linked list once and ensure that it is an atomic operation.
Actual corresponding state of this step: 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

First check g->gray, because of the existence of the luaC_barrier function (a mechanism for handling newly created objects), it may manipulate the variable g->gray when calling reallymarkobject.
2. Atomic completes the required 原子操作steps, mainly as follows:

  • 1 Re-traverse (track) the root object.
  • 2 Traverse the previous grayagain (there will be a weak table on grayagain), and clean up the space of the weak table.
  • 3 Call the separatetobefnz function to put the (white) objects that need to be recycled with the __gc function into the global_State.tobefnz table and save them for later cleaning.
  • 4. Make all objects on global_State.tobefnz reachable.
  • 5. Switch the current white value to a new round of white value.

The above content is just a brief description of atomic, there are many specific details that will involve too much content, and I will not expand it specifically due to space limitations.

4.5 Cleaning phase

STEP7: Recycle step by step according to different types of objects. Traverse the storage linked list of different types of objects in recycling
STEP8: Whether the storage linked list of the object reaches the end of the chain
STEP9: Determine whether the object color is white one by one
STEP10: Release the space occupied by
the object STEP11: Set the object color to white
The actual corresponding state of the above steps: 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

This process can be understood as garbage sorting and recycling :
GCSswpallgc will release all dead objects on g->allgc through sweepstep (objects with the GCSatomicprevious white value), and remark the live objects as the current white value.
The two states GCSswpfinobj and GCSswptobefnz also call the sweepstep function. But g->finobj and g->tobefnz cannot have dead objects ( 原因留给读者思考), so their role is only to reset these objects to a new round of white.
GCSswpend is used to release some space on mainthread, such as string table, connection buffer, etc.
The switching of these states is achieved by determining whether the current type of object linked list has reached the end.
The actual cleaning operation of the above state is completed by calling sweeplist through 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

The linked lists of various recoverable data type objects mentioned above are very long, so the removal is also done in segments. For example, the number of cleanings can be controlled by the count parameter in the sweeplist. (The setting of this value and the process of dynamic transformation, I hope to explain in the next article on GC parameter adjustment). From the above cleaning process, it can be understood that Lua's GC process is non-removable, that is, no data is migrated, and no memory is organized.
The last GC state: 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

In this state, the objects on the g->tobefnz linked list will be taken out one by one, and then called its __gc function, and put it into the g->allgc linked list, ready to officially reclaim this object in the next GC cycle.

5. Summary

At this point, in fact, Lua's GC algorithm has been described. But in order to echo the content of the algorithm brief at the beginning of the article after explaining the source code, it is also convenient for readers to have a macro understanding. So I want to finish the description of this algorithm with one sentence at the end: (Of course, the expression is only for the main situation)
Lua uses the grey linked list to sequentially use the reallymarkobject to mark the objects in color, and then traverse the alloc linked list and then use the sweeplist in turn. Clear objects that need to be recycled.

This blog post is reproduced at: https://blog.csdn.net/BigBrick/article/details/85317491

Guess you like

Origin blog.csdn.net/qq_43801020/article/details/108073978