Recent submissions

Comment · 9 months ago
Yes, I did actually look at Bunki as well before building this. Liked the imagery. :)

What did not quite fit for my purpose was having the asymmetric coroutines as the fundamental layer, meaning that the dispatcher also is tied to that layer. Building the next level dispatcher on top of that is probably possible, but seemed a bit complicated.

I instead started with symmetric coroutines as the fundamental building block and built the dispatcher at the next (process) layer where it can deal with the event queue to determine who goes next. The process level resume() call is always made from some event executed by the dispatcher on the main stack, making the processes asymmetric coroutines even if the lowest level coroutines are symmetric with transfer() calls.

Making asymmetric coroutines from symmetric ones is easy, just yield(void) { transfer(parent); } and resume(target) { transfer(target); } in slightly abbreviated code, while going the other way around requires an extra context switch via the dispatcher on the way there.

Comment · 9 months ago
Comment · 9 months ago
They probably do, but I have not used them. My approach has been "offensive programming", to put in asserts for preconditions, invariants, and postconditions wherever possible. If anything starts to smell, I'd like to stop it cold and fix it rather than trying to figure it out later. With two levels of concurrency in a shared memory space it could get hairy fast if bugs were allowed to propagate to elsewhere before crashing something.

Not familiar with the details of Intel CET, but this is basically just implementing what others call fibers or "green threads", so any such special handling should certainly be possible if necessary.

Comment · 9 months ago
Comment · 9 months ago
Good point. The profiler tells me that the context switch between coroutines is the most time-consuming part, even if I tried to keep it as light as possible, so I guess the explanation for "only" getting 45x speed improvement rather than 100x is that it is spending a significant part of the time moving register content to and from memory.

Any ideas for how to speed up the context switches would be welcome, of course.

Comment · 9 months ago
Each coroutine is running on its own stack. They are fixed size stacks, at least for now, so that could be a tender point, but I place some sentinel values at the end to try to capture it in an assert() instead of just letting it crash. I did not think it worth the effort and speed penalty to implement growing stacks yet. However, I do catch any coroutine function returns safely instead of letting them fall off the end of their stack.
Comment · 9 months ago
Hi, it is hand-coded assembly. Pushing all necessary registers to the stack (including GS on Windows), swapping the stack pointer to/from memory, popping the registers, and off we go on the other stack. I save FPU flags, but not more FPU state than necessary (which again is a whole lot more on Windows than on Linux).

Others have done this elsewhere, of course. There are links/references to several other examples in the code. I mention two in particular in the NOTICE file, not because I copied their code, but because I read it very closely and followed the outline of their examples. It would probably taken me forever to figure out the Windows TIB on my own.

What I think is pretty cool (biased as I am) in my implementation is the «trampoline» that launches the coroutine function and waits silently in case it returns. If it does, it is intercepted and the proper coroutine exit() function gets called.

Comment · 9 months ago
I would be happy accepting contributions, especially for porting to additional architectures. I think the dependency is relatively well encapsulated (see src/port), but code for additional architectures needs to be well tested on the actual platform, and there are limits to how much hardware fits on my desk.

Live from the official Hacker News API

A modern reader for Hacker News · SvelteKit + Skeleton