My experience with coroutines in Kotlin

Hi all,

I have successfully implemented my combinatorial optimization task using Kotlin coroutines.
Let me share my experience.

PROS:

  1. amazing eco-system for sequences, flows and channels
  2. the code based on coroutines is very laconic and well-readable
  3. no needs for extra structures like custom stacks – fun if you on combinatorial optimization!
  4. async out of the box

CONS:

  1. uncontrollable stack memory (not clear how to get neither the total stack size nor the size of the free stack)
  2. stack size is limited by 1MB
  3. stack overflow exception happens much more often than in programming without coroutines
  4. 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!

kind regards,
Valery

1 Like

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.

5 Likes

wow, very interesting, thank you!

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.

2 Likes

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?..

1 Like

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.

2 Likes

reasonable. But why then not like this:

  1. implicit TRO is on by default (, but could be turned off via compiler options if surprisingly needed)
  2. tailrec is rather a “modal” attribute for a function forcing compiler to spit error if TRO couldn’t be applied to the function
1 Like

I mean, currently tailrec modifier could be put in signature of every function. Even if it non-recursive at all. Without any side-effects.

So, the question is still there: why not to turn TRO on by default and apply it whenever it is possible?

1 Like

Also, 1M is generally the default stack size, not the maximum. You should be able to use -Xss jvm argument to expand it if needed (Ex: -Xss32M).