Criteria for clojures emitting new instances at runtime


#1

Hi everyone at the community of this awesome language!

I’m currently developing a game. There, you have to be careful not to create any instances after the loading screen is over and the game running. Otherwise, the GC may produce tiny lacks which prevent a smooth gaming experience. Therefore, I make use of object pools.

In some cases, Kotlin creates a lot instances at runtime, especially for closures. Sometimes even for IntRanges. Sometimes it doesn’t. I just want to know, how I can know how a piece of code will behave in advance.

So far I found out, it’s not a good idea to capture member variables. Are there any other rules?

Or are the rules changing a lot while Kotlin is developed. I have the feeling, that I had less trouble with 1.0.0-beta-2xxx (I’m using 3xxx now).


#2

I solved this issue myself. After reading Inline Functions, everything became easy. However, sometimes IntRanges are created when using for-loops. I thought they are replaced by for( ; ; ) style loops internally?


#3

If you’re having problems with GC pauses, try switching to the G1 collector and giving it an aggressive pause time target (e.g. 25 msec). G1 is quite capable of collecting even a fairly large heap with pauses low enough to fit inside a single frame at 60fps. As long as you don’t overload it, GC pauses should be quite tolerable and should not require you to systematically eliminate every possible allocation.

For instance, Unreal Engine uses a simple mark/sweep GC internally. It’s not even a fancy GC but for small game state heaps the pauses are low enough that users don’t notice.


#4

I’m going to try G1 on the desktop version of my game, but I don’t know how to force android to do so … ?

However, inlining functions solved the problem at almost most places in my code. Thanks to the paper above, I understood how they work and what they do. Still, some clojures produce a lot of *Ref instances, others don’t, It would be interesting to know in which situations the Kotlin compiler choose to wrap a captured variable in such an object and in which situations it can use it directly.

And the IntRanges are still a problem. I had to replace all for-loops which used …, until or downTo with corresponding while-loops, which is quite annoying, but does the trick.


#5

I see. G1 does not exist on Android. ART has a better GC than old Dalvik based devices do, but yes, my apologies, I should have guessed you were talking about Android.

A Ref is needed when you’re planning to write to a variable from inside a closure and the code isn’t inlined. As the closed over-code does not have the ability to write directly to the stack of the thread, the variable must be lifted from the stack into the heap where the closure would have access to it.


#6

Ok. I rewrote all my functions using closures so that the passed code is inlined or that they don’t use closures anymore at all.

When I started using Kotlin, I thought that the inline keyword is for optimizations, only. Just to avoid method calls for tiny pieces of code. Nice to know, that it also impacts how variables are captured.

for-loops with IntRanges are still causing trouble. I have no idea why they are always treated as Iterable and never be replaced by a classic counting loop.


#7

Currently the compiler should optimize the following two forms of the for-loop over a range:

  • for (x in left..right) where left and right are number primitives (int, long, char) – this is translated to the equivalent of Java’s for (int x = l; x <= r; x++)
  • for (x in range) where range is an expression of a numerical range or progression type (IntRange, LongRange, CharRange, IntProgression, LongProgression, CharProgression) – this is translated to the following:
    int first = range.getFirst();
    int last = range.getLast();
    for (int x = first; x <= last; x++) {
        ...
    }

If you use one of these forms and it’s not optimized for you, then this is a bug, please report.

Other popular forms are not fully optimized yet, in particular until and step extension functions. See the corresponding issues: KT-8901, KT-926. For example, if you use the a..b step c syntax, it leads to the creation of the intermediate Range object, passing it to the step extension function, creating another Progression object, and performing the second optimization from above to iterate on that progression.

However I’m not aware at all of the situation where we would treat an IntRange as an Iterable and iterate over it with an iterator. Could you please share some details on what code does that happen?


#8

Ok, good to know that until isn’t optimized yet because that was the form I used almost every time. I’m currently AFK for a while :frowning: , but I’ll check that when I’m back.