In the last installment, “How C Code Works,” we looked at how a high-level language, C, works.

C is also a high-level language, but it is still very “old” (nearly 50 years old). Comparatively speaking, the level of abstraction of C language is not high. From the expression ability of C language, you can still feel the shadow of hardware.

Narrator: In general, the higher the level of abstraction, the less mental work a programmer has to do when writing code.

Today we’ll take a look at how Lua, a relatively niche language, gets going.

interpreted

Unlike C code, the compiler compiles it directly into machine instructions that the physical CPU can execute, and the CPU executes those machine instructions.

The Lua code needs to be divided into two phases:

  1. Compile first to bytecode
  2. The Lua virtual machine interprets the execution of these bytecodes
Narrator: Although it is possible to take the Lua source code as input and get the execution output directly, the reality is that the two phases are executed separately internally

The bytecode

In “What CPUs Provide,” we looked at the two basic capabilities of a physical CPU: providing a set of registers that can execute a convention set of instructions.

Similarly, the Lua virtual machine also provides these two basic capabilities:

  1. Virtual register
  2. Execute bytecode
Lua register virtual machine, will provide virtual registers, the market more virtual machines are stack, do not provide virtual registers, but will correspond to the operand stack.

Let’s take a look at the bytecode with the following Lua code (yes, the logic is the same as the C code in the previous article). Compiling with Luac in Lua 5.1.5 yields the following results:

Lua main <simple.lua:0,0> (12 instructions, 48 bytes at 0x56150cb5a860) 0+ params, 7 lots, 0 upvalues, 4 locals, 4 constants, 1 function 1 [4] CLOSURE 0 0 ; 0x56150cb5aac0 2 [6] LOADK 1 -1 ; 1 # Loading the value (1) at position -1 in the constant region into register 1 3 [7] LOADK 2-2; 2 # Load the value (2) from the position (2) in the constant area into register 1 4 [8] Move 3 0 # Load the value of register 0 into register 1 Move to register 3 5 [8] MOVE 4 1 6 [8] MOVE 5 2 7 [8] CALL 3 3 2 # CALL to register 3, register 4, and register 5 as two function parameters Return value into register 3 8 [10] GETGLOBAL 4-3; print 9 [10] LOADK 5 -4 ; "A + b =" 10 [10] MOVE 6 3 11 [10] CALL 4 3 1 12 [10] RETURN 0 1 function <simple.lua:2,4> (1 instructions, 1 instructions, 2 instructions) 12 bytes at 0x56150cb5aac0) 2 params, 3 slots, 0 upvalues, 2 locals, 0 constants, 0 functions 1 [3] ADD 2 0 1 # ADD the values of register 0 to register 1 2 [3] RETURN 2 2 # RETURN 0 1

A little explanation:

  1. Unlike CPU-provided physical clusters, which have different names, bytecode virtual registers are nameless and numbered only numerically. Logically, each function has its own register, which is a serial number0Initial (there will actually be partial overlap multiplexing)
  2. Lua bytecode also provides the ability to define functions and execute functions
  3. The above output is in a human-readable format, but the bytecodes are actually encoded in very compact binaries (each bytecode is 32 bits long).

Execute bytecode

Lua virtual machine

The Lua virtual machine is a program implemented in C, the input is Lua bytecode, and the output is the result of executing these bytecodes.

Some abstractions in bytecode are implemented in the Lua virtual machine, such as:

  1. Virtual register
  2. Lua variables, for exampletable

Virtual register

For the virtual registers used in bytecode, the Lua virtual machine is emulated with a contiguous chunk of physical memory.

In particular, since Lua variables are stored inside the Lua virtual machine through the TValue structure, the virtual register is, in effect, a TValue array.

For example, the following MOVE directive:

MOVE 3 0

This is actually doing a TValue assignment, which is the C equivalent of Lua 5.1.5:

#define setobj(L,obj1,obj2) \
  { const TValue *o2=(obj2); TValue *o1=(obj1); \
    o1->value = o2->value; o1->tt=o2->tt; \
    checkliveness(G(L),o1); }

Its corresponding key machine instructions are as follows :(mainly through the MOV machine instructions to complete the memory read and write)

mov    rax,QWORD PTR [rsi]
mov    QWORD PTR [r9+0x10],rax
mov    eax,DWORD PTR [rsi+0x8]
mov    DWORD PTR [r9+0x18],eax

perform

The implementation of the Lua virtual machine has a for (;;) Infinite loop (in the luaV_execute function). Its core work is similar to that of a physical CPU, reading the bytecode of a PC address (simultaneously PC address +1), parsing the operation instructions, and then executing the bytecode based on the operation instructions and the corresponding operands. For example, the MOVE bytecode instruction we explained above is executed in this loop. Other bytecode instructions are executed in a similar way.

A PC pointer is also just a memory address of a Lua virtual machine location, not a PC register in the physical CPU.

function

A few basic points:

  1. Lua functions can simply be understood as a collection of bytecodes.
  2. In Lua, there are also stack frames, and each stack frame is actually a memory structure described by a C struct.

To execute a Lua function, execute its corresponding bytecode.

conclusion

Lua, a language with virtual machines, is logically similar to a physical CPU. The bytecode is generated and then executed by the virtual machine.

With an extra layer of abstract virtuality, bytecode interpretation can’t perform as efficiently as machine instructions.

Physical memory can read and write several times or even hundreds of times slower than physical registers (depending on whether the CPU cache is hit). As a result, reading and writing virtual registers in Lua is much slower than reading and writing real registers.

However, in Lua’s other implementation, Luajit, there is a great opportunity for this abstraction to be optimized. The main idea is the same as the GCC compiler optimization we saw earlier in “How C Code Runs”, which uses as many registers as possible and reduces the number of physical memory reads and writes.

There are a lot of great things about Luajit, but we’ll share them later.