Skip to content

Refactor various aspects of VM instructions

Yorick Peterse requested to merge refactor-instructions into master

Instruction memory layout

VM instructions now have a fixed size of 16 bytes, and no longer make use of a separate heap-allocated Vec for their arguments. This reduces memory usage, and should make for more cache-friendly instruction handling.

Each instruction is limited to six arguments, which is enough for all existing instructions. Instructions that need a variable number of arguments, such as SetArray, make use of register ranges. Instead of specifying all registers, they specify the first one and a length. The compiler in turn makes sure all argument registers are in a contiguous order. This approach is also taken by Lua, though unlike Lua we don't require the arguments to come after the register containing the block to run.

Some instructions supported optional arguments, such as SetObject. These instructions have been modified to simply always require an argument. This simplifies the VM code, and in almost all cases the arguments were always specified anyway.

For Inko's test suite, these changes reduce peak RSS usage from 27 MB down to 21 MB.

MoveResult instruction

Values returned and thrown are handled differently. Instead of each ExecutionContext storing the register (of the parent frame) to write their result to, operations that return or throw a value now store the value in a per-process "result" variable. The MoveResult instruction moves this value into a register, setting the "result" variable to NULL. This approach is inspired by the Dalvik VM, and simplifies instructions such as Return and Throw.

Single instruction for pinning processes

The instructions ProcessPinThread and ProcessUnpinThread have been merged into a single ProcessSetPinned instruction. This instruction behaves similar to ProcessSetBlocking.

Removed instructions

The following VM instructions have been removed as they were not used:

  • SetPrototype
  • RemoveAttribute
  • BlockSetReceiver

Refactoring of instruction handlers

The functions used for handling instructions have been refactored, renamed (after their instructions), and are now always inlined. This looks a bit funny at the moment, but it should make it easier for a future JIT to reuse these functions. The renaming also allows one to import specific functions, without having to worry about generic names such as "get" conflicting with other functions.

Bytecode parser cleanup

The bytecode parser has been cleaned up a bit, and now limits various data sequences to the maximum u16 value; instead of some arbitrarily determined limit. The u16::MAX limit ensures that registers can address the values directly.

Argument changes

The VM no longer supports keyword arguments and rest arguments. Instead, the compiler takes care of translating these to positional arguments. Keyword arguments are translated to positional arguments, with unspecified arguments being passed NULL pointers. The way this works is straightforward:

  1. Create an array with a NULL value/pointer for every expected argument. This is achieved by reserving register 0 and using that.

  2. For every positional argument passed, fill its corresponding cell. So argument 1 fills cell 0, argument 2 fills cell 1, etc.

  3. For keyword arguments, look up its argument position and set the corresponding cell.

  4. Pass this array as arguments to the VM instruction.

This is best illustrated with a simple example:

def foo(a = 1, b = 2, c = 3) {}

foo(b: 10)

Here the arguments passed would be:

[NULL, 10, NULL]

The VM then checks if b and c are set, sees they are NULL, and assigns them their default values.

Validating of argument counts is also removed from the VM, now that dynamic method calls are no longer supported.

Edited by Yorick Peterse

Merge request reports