Luau Internals 101

Prerequisite Knowledge

  • C concepts: structs, data types (int vs. uint)
  • Memory addresses
  • Basic knowledge about bytecode & interpreters
  • Values vs References

Starting facts

  • Luau values are register-based. There are different registers that bytecode writes and reads to.
  • Luau compiles to bytecode
  • Each Luau register is a “Lua object” which is 16 bytes¹
  • Luau is written in C++. However, Luau also uses barely any C++ features, and I will often abbreviate it down to things like “C-sided” for this post.

Comment directives

There’s a few comment directives:

  • optimize, possible values: 0, 1, 2.
    • Optimize level 2 has things like inlining. Optimizations which hurt debuggility.
    • Optimize level 1 has optimizations which rely on assumptions Luau makes, involving things like constant folding.
    • Optimize level 0 has no Luau optimizations.
    • safeenv is a C-sided variable which determines if it’s okay to be in optimize levels 1/2. loadstring, getfenv, setfenv force safeenv to be false, which will cause the code to run at optimization level 0.
  • native
    • This enables native code generation, which means your code will be compiled to native instructions and ran without running through the interpreter.
    • Native code generation is actually really good.
  • --!strict, --!nocheck, --!nonstrict, etc. won’t be covered cause they don’t affect runtime behavior.
    • Type information is taken into account by syntax. So the typechecker isn’t running³, it just checks the annotations.

Luau bytecode example

    1: local left = math.random(1, 2)
GETIMPORT R0 2 [math.random] -- Load the C function for math.random into Register 0
LOADN R1 1 -- Load the number 1 into Register 1
LOADN R2 2 -- Load the number 2 into Register 2
CALL R0 2 1 -- Call whatever is inside Register 0
    2: local right = math.random(1, 2)
GETIMPORT R1 2 [math.random] -- The observant of you might have noticed Luau reusing registers here.
LOADN R2 1 -- Brownie points if you did, I guess
LOADN R3 2
CALL R1 2 1
    3: local value = left + right
ADD R2 R0 R1 -- Store the result of adding Register 0 and Register 1 in Register 2
    5: print(value)
GETIMPORT R3 4 [print] -- Get the print function, store it in Register 3
MOVE R4 R2 -- Not totally sure what this move is here for, tbh?
-- Maybe an unused register getting moved down.
CALL R3 1 0 -- Call the print function
    6: 
RETURN R0 0

What is a “lua object”?

A Lua object is either a value (the number 50, the boolean true) or a gc object (gc standing for garbage collector). The following are gc objects:

  • Strings
    • Strings are gc objects because they are hashed & interned (this is so that the same string doesn’t take up memory twice)
    • There is a sweep of unused string hashes that gets cleared.
  • Buffers
  • Tables

The following are NOT gc objects:

  • Vectors
  • Numbers
  • Booleans
  • nil (obviously)

The important thing to note about every single gcobject is that they are stored on “the heap”. Meaning that the actual value isn’t stored in the register, but rather the value is a pointer to the actual data. So doing the following: local value = "hi", won’t put “hi” in a Luau register, it will put it in a hash table.

As for the actual garbage collector itself, there is some very nice documentation on it here.

You can think of any gcobject value as basically a pointer to the real information.


Tables

Tables are stored into two portions: The array portion, and the hash portion. The array portion is stored continuously in memory, in order to optimize for cache locality.

This file has nice documentation on the inner workings of tables.


C-Luau barrier

Any C-sided code which invokes Luau code has to go through an API which I dub the “C-Luau barrier”. It is an API which lets external code interact with your Luau code. Example of such API in use²:

static int vector_floor(lua_State* L)
{
    const float* v = luaL_checkvector(L, 1);

    lua_pushvector(L, floorf(v[0]), floorf(v[1]), floorf(v[2]));

    return 1;
}

Now this does lead into another good question, what is this “lua_State”?


What is a “lua_State”?

…A lot. You can think of a lua state as the “active state of the VM”. This is where information about things like upvalues, the stack trace, etc. etc. are stored. Lua states are bulky, big, and generally a great optimization tip is to avoid areas where you can be creating new Lua states. So you might ask: What creates a new Lua state?

The answer to that is simple: A closure. Avoid creating closures. Pretty simple optimization tip, eh?


Disclaimers

  1. I am not actually 100% sure about that 16 bytes figure, that’s reversed and taken from things like numbers. There may be exceptions, it may vary. It isn’t that important and you shouldn’t rely on it (i am not sure how you would.)
  2. I removed the macro for 4-wide vectors because it’s irrelevant to the example and adds verbosity to it. The source code is from this file.
  3. There are active plans to make inference have optimizations at runtime. This likely will not hold true for very long.
15 Likes