I have successfully implemented my combinatorial optimization task using Kotlin coroutines.
Let me share my experience.
PROS:
amazing eco-system for sequences, flows and channels
the code based on coroutines is very laconic and well-readable
no needs for extra structures like custom stacks – fun if you on combinatorial optimization!
async out of the box
CONS:
uncontrollable stack memory (not clear how to get neither the total stack size nor the size of the free stack)
stack size is limited by 1MB
stack overflow exception happens much more often than in programming without coroutines
code profiling is quite unusable even with coroutine agent enabled for debugging
All PROS are a pure fun.
Let me elaborate on CONS part.
1., 2. and 3. ==>
Heavy use of coroutines means burdening the stack memory, not the heap.
However stack sizes are very modest, e.g. -Xss for JVM has maximum value of 1MB, which is a huge difference to the heap sizes.
Any attempt to move the data structures from stack to the heap would perhaps make the source code bloated, less readable (and probably also slower because heap is usually not that fast on allocation/deallocation)
4 ==>
profiler output is flooded badly with internal kotlinx.coroutines.flow.FlowKt* entries, which are not explicitly tied to my corresponding coroutines. A line-profiler could have been of a huge help here (but is any of them available?)
profiler can’t help answering the main headache question during use of coroutines: which coroutine eats my stack memory?
If you have any ideas on how to cope with these CONS – please share!
I’m surprised with your findings about the heavy stack usage. I would actually expect the opposite. Coroutines in Kotlin are stackless and that means inactive stacks are stored entirely on a heap. And active stacks are generally shorter, for example you can be 20 levels deep into the call stack, but due to coroutines you are actually only 3 levels deep. Making the code suspendable adds some memory overhead, but I think it shouldn’t be high, probably just a single extra object reference per a function call (continuation).
Could you share an example of code that caused you such problems? Are you sure that was not caused simply by using recursion? I ask, because when we write concurrent code using e.g. executors, it is pretty much impossible to use recursion, because we have to schedule each task separately to the executor. With coroutines we can write sequential code, so it is much easier to use recursion.
I can’t share the code, it is not mine anymore, it belongs to my client :-/
In my combinatorial optimization I use a composition of several branch-and-bounding implementations tangled together to traverse the combinatorial space smartly.
Branch-and-bounding algorithms are often just a back-tracking with smart cutting.
Back-tracking is much easier to implement in recursion (no need to introduce a stack explicitly).
So, in my case there are several recursive suspending functions, which emit all the found valuable stuff to the caller/subscriber.
You are likely right very much, when you say, that stack is eaten up not by the coroutine nature of my functions, but by their recursive nature.
Just FYI, you probably already know this, but you can use the tailrec modifier in Kotlin to define tail-recursive functions that call themselves at the end of their execution, and the compiler transforms the function into a loop.
Also, you can use the DeepRecursiveFunction utility from the stdlib, but sadly that doesn’t seem to support suspend functions.
Thank you!
I did know about tailrec, but didn’t hear about deepRecursiveFunction, interesting.
Well same interesting is why should programmers think about tailrec if tail recursion is easy to detect. Is it like sometimes tail recursion optimization isn’t wanted?..
It might be easy to detect for the compiler but perhaps not for the programmer. If tail recursion would be implicitly enabled, it would also be implicitly disabled. Superficial changes to a function could disable the optimization and result in StackOverFlowExceptions being thrown.