Disclaimer: There are multiple implementation techniques for both stackful and stackless coroutines, each with their own strengths/weaknesses. I do not promise to consider all possible implementations.
Memory Allocation
A separate stack typically needs to be allocated for each coroutine; this is fine for a long-lived one, but costly for rapid-fire one-off coroutines. It's even worse for languages where each stack frame is allocated separately.
On the other hand, no memory allocation is necessary for stackless coroutines, though certain implementations do allocate regardless (C#, for example).
Memory Consumption
A separate stack generally consumes more memory.
- Go: the compiler does not "prune" a stack frame before calling the next function, so "junk" accumulates at each frame, and the stack also typically features some "excess" capacity, to avoid constantly allocating/deallocating.
On the other hand, stackless coroutines tend to boil down to (nested) state machines, and only variables required across suspension points are saved, naturally trimming them down on the go.
Memory Cache (Misses)
A separate stack is less likely to have its current stack frame in cache, whereas a stackless coroutine reuses the existing (already in cache) stack.
Suspension/Resumption Performance
Suspending or resuming a stackful coroutine is generally O(1), though not lightweight:
- Go: registers need be saved, then the stack pointer switched to that of the new coroutine, and its own registers restored.
On the other hand, suspending or resuming a stackless coroutine is generally O(N) where N is the number of stack frames to suspend or resume. Each step is typically lightweight, but combined they may outweigh the heavier O(1) cost of a stackful coroutine.
Different situations will favor one strategy over another.
Suspension/Resumption FFI
A stackful coroutine may be FFI compatible. For example, it is possible to call C code from Go code, then call Go code from the C code, and suspend there, thus suspending the C stack frames even though the C compiler was not even aware that suspension was a possibility.
It is not typically possible to suspend a stackless coroutine with foreign stack frames in the middle, as the compiler of the stackless coroutine language does not know how to pack the state of those frames.
Debugging
It is trivial for a debugger to display the stack of a stackful coroutine, as it's usually just a regular stack as far as the language implementation is concerned.
On the other hand, it is nigh impossible to do so for a stackless coroutine. Any stack information prior to the last resumption is generally lost -- they did not survive the transformation to state machine and back -- and the caller of the resumption may be a completely different caller from the original one, possibly even on a different thread.
Optimization
Stackful coroutines are lightweight threads, and typically not optimized away, but each individual stack can be optimized to the full extent of the compiler capabilities.
Stackless coroutines implemented as (nested) state-machines in the front-end are trivially optimized away -- when possible -- via the regular inlining/constant propagation passes of an optimizer. On the other hand, the fact that the front-end performs the lowering may inhibit optimizing each stack frame (across suspension points) such as introducing temporary variables (CSE) or vectorizing.
Stackless coroutines implemented as (nested) state-machines in the back-end (C++) are harder to optimize away -- due to the presence of user-defined dynamic code -- though the possibility has been demonstrated, on the other hand each stack frame may be optimized fully (on its own).
Different situations will favor one strategy over another.