Skip to main content

GC differences — theoretical comparison

Nature: theory, not a benchmark. ZLua details: follow spec/10-LIFETIME.md, spec/marshal/.


1. Analysis axes

AxisQuestion
Hot-path allocationDoes every Lua↔C# call force object[], boxing, new string?
userdata vs managed objectsOne-to-one? Weak tables? registry slots?
structCopy / pooling / Opaque temporary handles
StringsDoes Push/Pop allocate a new managed string every time?
Delegate / closuresBridge object lifetime and refs
Peak vs steady stateFirst Bind / first delegate vs hot loop

2. Four-way hot-path allocation profiles (typical)

2.1 xLua

PathTypical allocation
Lua→C# simple methodGenerated Wrap usually no object[]; cleaner with value-type args
Lua→C# overload / reflection fallbackMay object[], params boxing
C#→LuaLuaFunction.Call etc. may allocate; many LuaDLL calls
userdata ↔ objectObjectTranslator pool + weak refs; Pushing new objects may allocate wrapper info
stringLua string ↔ UTF-16 usually allocates a new string
structOften boxing or table staging (depends on Wrap)
DelegateDelegateBridge + translator entries; first bind allocates

“Zero GC”: neither official nor community promises hot-path zero GC; blittable hot loops can approach zero alloc; string/object always allocate.

2.2 toLua / tolua#

PathTypical allocation
Wrap callsSimilar to xLua; earlier versions rougher on hot paths
Out / refOften tables or multi-return; may temp tables
string / objectSame order of magnitude as xLua
structDepends on export strategy; boxing common

“Zero GC”: not a general promise.

2.3 SLua

PathTypical allocation
Auto-bindSimilar to toLua
Value-type optsSome versions optimize structs; still export-dependent
DelegateLuaFunction conversion often allocates

“Zero GC”: not general; needs per-API profiling.

2.4 ZLua (Il2Cpp Player design goals)

PathTypical allocation
Lua→C# blittable methodsC++ bridge reads stack → methodPointer; target zero GC
Lua→C# string / classstring alloc; class ObjectRegistry Push (slots + weak cache)
C#→Lua GetFunction blittableDelegate bridge PushDefault*; target zero GC
C#→Lua ref/outOpaqueValue (lightuserdata, no managed boxing)
struct ByValpayload copy inside userdata; non-blittable may have boxed companion
Delegateclosed delegate + funcRef; first bind allocates; steady state TBD
Mono Editor EmitDesigned for no hot-path object[] + Method.Invoke; semantics match Player

3. ZLua core mechanisms

3.1 ObjectRegistry (ByObj)

Manages ByObj userdata for class / string / array / delegate / boxed enum, etc.:

Push → allocate slotIndex → _registeredObjects[] + GC root (Il2Cpp)
→ weak-value cache (obj, viewKlass) → avoid duplicate Push
__gc → UnregisterObject → drop root
ItemGC meaning
slot + rootWhile Lua holds userdata, prevent Il2Cpp from collecting that object
Weak cacheHit → no new Push; miss → one Push cost
PopNo alloc; slot lookup only

See spec/10-LIFETIME.md §2.

3.2 ByVal struct

ShapeAllocation
ByVal userdatapayload copied inside userdata; __gc frees native copy
non-blittable ByValMay boxed companion + NotBlittableStructRegistry scan
zlua.box → ByObjboxing to ByObj via ObjectRegistry

Lua→C# struct args: default ByVal copy; not boxing every time (unless ByObj path).

3.3 OpaqueValue (C#→Lua)

ItemNotes
Shapelightuserdata handle pointing at C# ref slot
LifetimeOnly that C#→Lua call frame; save across pcall → error
GCHandle itself does not bump managed object counts; pointed ref slot valid during invoke

Used as the default path for GetFunction delegates / delegate bridge ref/out/in (see spec/marshal/04-OPAQUE.md).

3.4 Indexer & allocation

ModeAllocation impact
Il2Cpp Dispatch* indexerC++ path; no extra alloc because of indexer
Mono three-table Lua indexerPure Lua table lookup; miss returns nil, no temps

4. Boundaries of “zero GC” claims

Here “zero GC” means steady-state hot loop, GC Alloc ≈ 0 (Unity Profiler / dotMemory sense), not absolute absence of native malloc.

4.1 ZLua paths that can approach zero GC (Il2Cpp Player)

ConditionExample
Fully blittable signaturevoid Tick(float), int Add(int,int)
Lua→C# with no new string / classnumbers + existing userdata only
C#→Lua blittable returnsint, float, no string
Already Bound; no first EnsureBindingInside hot loop
No cross-frame Opaque for ref/outOpaque only within a single invoke

4.2 Paths that necessarily or almost always allocate

PathAll four solutions
new string across boundaryMay allocate UTF-16 string
new class returned from C# to LuaZLua ObjectRegistry Push; xLua translator
Boxing enum / struct (if ByObj)Possible for all
First type Bind / delegate bindPossible for all
Lua table ↔ C# collectionDepends on [LuaMarshalAs] / API; often allocates
Overload dispatch fail/retryMay temp objects (TBD)

4.3 Four-way “zero GC” honesty check

ClaimValid range
xLua hot path can be optimizedNeeds Generate + avoid reflection; not global zero GC
toLua / SLuaGenerally do not advertise zero GC
ZLua Il2CppOnly blittable hot paths target zero managed GC; string/object/first delegate bind no

5. Peak vs steady state

PhasexLuatoLua / SLuaZLua
First type accessWrap ready if Generate doneExport already generatedEnsureBinding + stub register (Il2Cpp)
First Push of an objecttranslator registerpool registerObjectRegistry slot + cache
Hot loop 1M × P1Alloc should ≈0 (good Wrap)SimilarTarget ≈0
1000 string APIs / frameAlloc + GC pressure dominateSameSame

6. Delegate & Lua ref lifetime

SolutionModel
xLuaDelegateBridge holds refs; Dispose / GC at the right time
toLua / SLuaManage LuaFunction lifetime manually
ZLuafuncRef + closed delegate; ProcessPendingRefReleases frame-pump delayed release (spec/10-LIFETIME.md)

Migration note: don’t assume a Lua function frees immediately when the C# delegate is unreferenced; understand ZLua ref-release semantics.


7. Il2Cpp GC integration (ZLua-specific)

MechanismPurpose
ObjectRegistry slot GC rootLua userdata alive → managed object not collected by Il2Cpp alone
non-blittable struct push_other_rootsScan reference fields inside struct memory
userdata __gcUnregister in sync with Lua GC

xLua / toLua / SLua generally do not change Boehm/Il2Cpp GC root policy; ZLua intentionally integrates for correctness.


8. Analysis tips (engineering)

  1. Pick the scenario first: P1-style hot loops vs P6-style string-heavy.
  2. Profiler Alloc: if every call shows System.String / Box / object[], interop optimization pays little.
  3. Run both ends: Mono and Player GC may differ; Player is truth.
  4. Read the Spec: struct/ref paths in spec/marshal/03-BYREF.md, 05-STRUCT.md.

DocContent
PERFORMANCE.mdMeasured performance summary
impl/marshal/REGISTRIES.mdRegistry implementation
FEATURES.mdValue-type usage differences

Theory draft; same-scenario GC Alloc comparison across four solutions TBD.