Ruby currently uses OS provided abstractions for implementing Fibers. Unfortunately, these often perform poorly and have tricky semantics. We present a native implementation of coroutines and demonstrate improvements to performance.
The most common implementation on UNIX systems involves manipulating the state of
swapcontext(). These function calls have been deprecated and removed from the POSIX standard, but still exist in most UNIX implementations for backwards compatibility. As well as being poorly implemented and supported, the documented semantics of these functions requires certain system calls which limit their performance.
Another typical undocumented and unsupported approach is to abuse
longjmp(). It is possible to manipulate the
jmp_buf to change the return address and stack pointer, so it is possible to jump between fibers.
Windows provides native APIs for fibers and these are pretty decent. They work as expected, but depending on the situation, may do more than required at the expense of performance.
A native implementation of coroutines using assembly can significantly improve performance of the Ruby's Fibers. Even thought some assembly is required, the net semantic complexity is reduced and existing hacks can be removed.
The state that is required per coroutine - typically just the stack pointer, but it is possible to augment this with other per-coroutine data:
The initialization function prepares the stack so that when we transfer to it, it will return to the given start address:
The destroy function essentially just nullifies the stack pointer:
The transfer function switches between coroutines. It essentially saves the caller state onto its stack, swaps the stack to another coroutine, and then returns.
Keep in mind that this is the simplest possible implementation and doesn't preserve things like sigmask, FPU state, etc. They essentially remain per-thread with this specific implementation.
Generally speaking, the overhead of a fiber context switch is not much more than a standard C fast call (pass as many arguments in registers as possible). On x64, the standard (and only) ABI is fast call, and so it's efficent by default.
The code is available here and the Ruby bug report has more details. There is a PR tracking changes.
The goal of these improvements is to improve the performance of async. I've measured a 5% improvement to async-http.