Coroutine Implementation - mfichman/jogo GitHub Wiki

A coroutine is a stateful generalized subroutine that allows execution to be suspended. When a coroutine is suspended, it's execution state is saved, so that it can be resumed later. The coroutine execution state includes:

  • Contents live registers
  • Stack pointer (often a register)
  • Stack

When a coroutine is started in Jogo, it is allocated a stack (by default, 16KB). This stack is much smaller than a normal stack (usually 1MB for Windows and 10MB for Linux). As a result, Jogo coroutines are designed to grow.

New Implementation

When a new coroutine is created, stack space for it is reserved, but not committed, using the virtual memory support provided by the operating system. On Windows, VirtualAlloc(MEM_RESERVE) is used, and on Linux/OS X, mmap(PROT_NONE) is used. Since the coroutine stack starts off as reserved memory, the first access to untouched regions of the stack generate a segmentation fault (AKA access violation). The segmentation fault is handled by asking the operating system to commit the page containing the faulted address, and then restarting the program where the fault occurred. Each segmentation fault is checked to see if it falls inside the valid stack range of a coroutine.

This implementation allows the stack to grow dynamically without the stack check preamble used by the old approach (better performance). As a downside, coroutine stacks cannot be arbitrarily large; however, given the 8 TB 64-bit address space on most operating systems, there is enough address space for 104,857 10-MB coroutine stacks (of course, if all those coroutines are active, the system would quickly run out of real memory anyway).

Old Implementation

At the start of each function, Jogo emits a stack check preamble which checks to make sure there is enough space on the stack for all local variables. If there isn't enough space, a new coroutine stack segment is allocated. The stack check preamble looks like this (x86-64):

    mov rax, [rel Coroutine_stack]
    add rax, MinStack
    cmp rsp, rax
    jge .FuncStart
    call Coroutine__grow_stack
    mov rsp, rax
.FuncStart:
    ...

In C-like psuedocode, this translates roughly to:

if (rsp < Coroutine_stack+MinStack) {
   rsp = Coroutine__grow_stack();
}

Coroutine_stack is the bottom of the current stack (stacks for x86 systems grow down towards lower addresses). For example, if rsp = 1000, Coroutine_stack = 500 and MinStack == 800, then 1000 < 500+800 and a new stack segment is allocated by Coroutine__grow_stack(). In other words, the amount of space left on the stack is 1000-500 = 500, which is less than the MinStack, or 800 in this example, so the stack must grow.

Now, in the case of native calls, a MinStack of 800 would not be nearly enough. This is because native calls may call other functions that use up lots of stack, and the Jogo compiler has no way to tell how much stack is necessary. This could result in a stack overflow. To get around this, a larger stack is needed for native functions. Jogo switches to the original program call stack (the main stack) for native calls, because this stack is very large (around 1-10MB). In essence, every native function call is doing a context switch to the main or native coroutine. The function call sequence works for native calls works like this:

  1. Get a pointer to the main stack
  2. Store the current stack pointer on the main stack
  3. Store function arguments on the main stack
  4. Modify the stack pointer to point to the main stack
  5. Call the native function
  6. Pop results off the main stack
  7. Restore the original stack pointer