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.
Existing Implementations
The most common implementation on UNIX systems involves manipulating the state of makecontext()
and 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 setjmp()
and 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.
Improving 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.
x64 Implementation
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.
Performance
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.
Further Reading
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.
Comments
That got me poking around as I had plans for them….
So I asked a question on the libc-help mailing list and got this response… https://sourceware.org/ml/libc-help/2018-06/msg00011.html
I didn’t find anything on the Austin groups bug tracker except…. http://pubs.opengroup.org/onlinepubs/9699919799/xrat/V4_xsh_chap03.html#tag_22_03_01_07
Which IMHO is not a very satisfactory answer.
In my experience of playing around with
swapcontext()
, the behaviour of signals and sigprocmask were certainly a source of pain (and in the presence of the early spectre / meltdown mitigations, a huge slowdown).Unfortunately I think POSIX has invested so much sunk cost into pthreads they no longer have the will to step away from it. I wish they would.
Leave a comment
Please note, comments must be formatted using Markdown. Links can be enclosed in angle brackets, e.g.
<www.codeotaku.com>
.