Cooperative Threading
byuu.net
byuu.net
A big penalty is due to (failed) branch prediction: a coroutine switch is essentially an indirect branch so the predictor might need help (for example using a different branch instruction address for each coroutine type); also call instruction used to call into the coroutine switch function is not paired with a ret instruction, which messes with the specialized call predictor. Changing the final jmp instruction in the switch function to push add; ret actually makes thing worse; the best solution is to inline the switch function in the caller via inline assembler (this also helps with the previous issue and, if the compiler provides the functionality, it allows only saving the registers that are actually in use)
edit: last time I did a synthetic benchmark of my one of my own coroutine implementations, the switch performance was only constrained by the number of taken branches the CPU could issue (one every other cycle): https://github.com/gpderetta/delimited/blob/master/benchmark...
[1] I'm talking about the typical x86 cpu.
I did also try inlining this, but it was much less portable and actually performed slower in my emulator. I won't claim that will be the case for everyone, of course. YMMV.
This is my old naive version:
https://github.com/ademakov/MainMemory/blob/master/src/base/...
This is what I have now:
https://github.com/ademakov/MainMemory/blob/cstack-switch-re...
I thought that was (essentially) the definition of longjmp? Thinking further, it seems like the initial setup of additional stacks would require at least taking the covers of setjmp and interacting with its implementation details directly.
https://pubs.opengroup.org/onlinepubs/009695399/functions/ge...
Marked obsolescent in that version (2004 edition) (purportedly replaced by POSIX threads, except those aren't coroutines...) — but likely still present in whatever libc you use. (There's also pthread_set_concurrency(), but it is (a) option and (b) marked obsolescent as well ... https://pubs.opengroup.org/onlinepubs/9699919799/functions/p... .)
The Linux man page explains excellently:
"POSIX does not specify whether setjmp() will save the signal mask (to be later restored during longjmp()). In System V it will not. In 4.3BSD it will, and there is a function _setjmp() that will not. The behavior under Linux depends on the glibc version and the setting of feature test macros. On Linux with glibc versions before 2.19, setjmp() follows the System V behavior by default, but the BSD behavior is provided if the _BSD_SOURCE feature test macro is explicitly defined and none of _POSIX_SOURCE, _POSIX_C_SOURCE, _XOPEN_SOURCE, _GNU_SOURCE, or _SVID_SOURCE is defined."
Embedded C programmer here. I find this statement highly offensive :D
I wonder if this guy knows how planes and car and traffic lights and medical equipment all works? Lots of things you rely on every day were made in languages that allow “insane” direct CPU register access.
What do you think you do when you write in assembly and want to enter or exit a function? You push and pop on the stack control registers.... gasp?
It’s not common but I’ve had to manually adjust PC register before. The author and most software devs are so far removed from metal, the downvotes on my comments are hilarious to me.
The author writes emulators of multiple game systems in C++. They probably wake up screaming at night thinking about the metal.
My point was, if C++ had a global variable named "stack_pointer" you could assign to, that would indeed be rather insane.
You said you were an "Embedded C programmer". Why are you now switching the topic and talking about assembly? First of all there is no such objective definition of assembly. It is inherently specific to the architecture and therefore someone knowing x86 assembly doesn't mean that person also knows how to use RISC-V assembly. Since you have moved the goalpost out of the playing field by switching languages one could now conceive of an architecture that simply has no registers at all because everything is stored in RAM. Such an architecture would allow a C compiler to still produce valid code but assembly code would not be able to access any registers whatsoever. Therefore it makes equally little sense for the C programming language to have the ability to access registers.
> The author and most software devs are so far removed from metal, the downvotes on my comments are hilarious to me.
The reason why you receive downvotess is that even people who are not "far removed from metal" disagree with your comments because you are making fun of them.
https://www.forth.com/resources/forth-programming-language/
"a PDP-11 or Nova could be expected to support up to eight users, although the performance in a system with eight active users was poor.
On this hardware, Moore’s Forth systems offered an integrated development toolkit including interactive access to an assembler, editor and the high-level Forth language, combined with a multitasking, multiuser operating environment supporting 64 users without visible degradation..."
The FPGA version is about 40 times or so faster than the model and the actual silicon is another 10 times or so faster than that.
Any chance we standardize and commoditize fpgas so that it can be just another device on a laptop or phone that anyone could use? Like bluetooth or a GPU or gps?
This has to happen at some point but how far off from that are we now?
I assume there are different grades of fpgas and that likely complicates things but it is like desktop GPUs are way way better than phone GPUs... But both run opengl/vulkan and essentially the same code.
Edit: this [2] is a full system for 220 euros and in addition to Amiga it emulates a bunch of 8 bit consoles.
[1]: https://en.wikipedia.org/wiki/Minimig
[2]: https://amigastore.eu/en/358-mist-midi-fpga-computer-with-mi...
The upside to this is that Verilog makes it very easy to model parallel hardware. The downside is that you have to model the hardware at the clock-cycle, "flip-flops and Boolean logic" level of detail.
Now, that's not to say that it wouldn't be good enough, i.e., fast enough that your games would still be playable. It's an interesting idea.
Fundamentally, preemption has to originate from a hardware IRQ (or IPI from another core), so really the only way would be to kernel-bypass by setting an IRQ handler in userspace (ring 3). That's technically possible on x86 (IDT entry can have a ring-3 code segment) but I don't think the kernel has a mechanism for that...
If you want to fake preemption in userland, you have to add a 'preemption' check every so often. Erlang uses the number of function calls made (reductions), which works because it doesn't have a loop concept, just recursion, which is a function call. As long as you have a check happening frequently enough, you can count the number of calls, or do something cool with rdtsc to track time or ? (Being careful not to use a syscall that may do a context switch you were trying to avoid).
Cooperative multitasking is essentially having to put the preemption checks in explicitly. For an emulator, it seems that it's not too hard to piggyback that on cycle tracking. Although it probably depends on the target system's design; some machines have lots of chips in tight lockstep, and some are more asynchronous with smaller overlap.
The preemption check would simply write some data to memory (the actual data wouldn't matter), and the watchdog would use the MMU modification flag to check if the memory page was updated. That way you'd avoid synchronisation problems.
On the other hand, dedicating a core as a preemption supervisor seems wasteful. I guess you could run the supervisor on an hyper thread to reduce wastage.
The problem is 2-fold; (1) is that in cooperative (userspace) multithreading, context switches can only happen at a safepoint, where the runtime knows what's going on, and what the current state is; that's necessary for any kind of userspace service provided by the runtime, such as GC, synchronisation, JIT, etc. I think JVM people (or maybe Go? edit: found the Go issue [0]) are trying to generalize this so that every point of the program is a safepoint; not sure how it's going. (2) is that userspace has very limited capacity to even observe OS-level context switches, let alone modify them... E.g. ideally you'd be able to take a look where (at which instruction) the context switch happened, what the current state is (e.g. values of registers), and maybe modify it (e.g. switch to another coroutine). AFAIK there's no portable way to do this, and some OSs/platforms don't support any way to do this. Basically pretty much the only thing that can happen after a thread is interrupted by the OS, is that the thread resumes exactly when it was.
- Userspaces cooperative scheduling for all the tasks, in multiple threads if multi-core.
- A periodic timer signal sent to all the userspace threads. (Or one thread, but you might have to give it high scheduling priority; not all kernels offer this)
- The key to keeping kernel-userspace transition overhead down is the periodic timer doesn't run too often.
- The timer signal handler checks the per-thread "context-switched since last check" flag. If set, clear it. If not set, either pre-empt the active co-operative task in that thread (if that's possible; see another comment about safepoints), set a "pre-empt soon" flag for the co-operative task to detect (e.g. if it's looping but checks the flag in the loop), or move the inactive tasks in that thread to another thread (work-stealing), or a new thread (using pre-allocated idle threads because you can't make a new thread in a signal handler).
- If the timing of the timer signals is inconvenient, for example if you want to give the active task about 15ms more execution time, the signal handler may start a second timer instead of immediate action.
- Checking a per-thread flag from a signal handler may prove entertaining to do in a standard async-signal-safe way. (This matters more on older platforms where threads are themselves implemented in all sorts of ways.)
- Waking an idle "monitor" thread with a timer would be a cleaner way to do some of this, but there's no guarantee about when the monitor thread will run, and it may be a while if the CPUs are already busy. RT priority helps, if available.
https://stackoverflow.com/questions/2708033/technically-why-...
I wanted one because in 2007, no project existed that was laser-focused on maximizing performance (I switch threads tens of millions of times a second in my emulators), was lightweight enough (I implement my own schedulers for my emulators), and was portable enough (I am not even aware of a CPU architecture libco won't run on currently.)
The library is extremely small, and being in control of it allows me to adapt and support new targets directly as needed.
for(EmulatedChip &chip : chips) {
chip.emulate(time);
}
Why would the above not be suitable for emulation?You start writing a new emulator for the first time, dispatching entire instructions in one go, and so you don't even really need a state machine at all.
Then you start trying to get the timing better with opcode-cycle granularity, and in comes a separate state machine for every instruction.
Then you realize you need clock-cycle granularity to fix certain edge-case games (eg emulating the effects that occur during bus accesses), and suddenly the prospect of a state machine for every cycle of every instruction becomes overwhelming.
You would then be realistically stuck with the choice to either stop improving your accuracy, or rewriting things cooperatively.
Since I'm focusing byuu.net articles toward aspiring emulator developers, I thought it would be an important topic to cover.
If you just have a bunch of chips operating separately without the need for synchronous communication your model works, but that's unrealistic.
This seems to be incorrect. See:
https://stackoverflow.com/questions/28977302/how-do-stackles...
> In contrast to a stackless coroutine a stackful coroutine can be suspended from within a nested stackframe.
It seems the author is trying to reinvent stackful coroutines, and calls them "cooperative threads".
A better name for stackful coroutines is one-shot continuations.
In any case, I am not so much trying as I already have. C++ does not have stackful coroutines. I implemented them via my libco library and have been using them for over a decade now.