Int range or object inside filter

Hello, I worry about memory usage and unnecessary object creation / destruction. If I have:

myList.filter{ it.x in 1…100 }
or
myList.filter{ it.x in a…b }

Am I correct kotlin would instantiate an IntRange for every test of the filter? (and then gc)

Assuming Kotlin has some workaround in the back where it will optimize this to:
val range = a…b
myList.filter{ it.x in range }

or better:
myList.filter{ it.x >= a && it.x <= b }

can I still “break” it by having
myList.filter{ it.x in a()…b() }
or
myList.filter{ someObject(it.x).getBoolean() }

and yes in those last two I am the main source of the problem, but it’s just so I understand this

Not only for this specific case, but in general x in a..b is compiled into x >= a && x <= b. It will create a range object if we actually create a local variable, pass a range between functions, etc.

it.x in a()..b() doesn’t break it, meaning that it still doesn’t create a range, but do: it.x >= a() && it.x <= b(). Of course, we call a and b with each iteration, but we asked for this.

Also, please note it.x is acquired only once. So this is not exactly it.x >= a && it.x <= b, but more like: val x = it.x; x >= a && x <= b.

2 Likes

thank you

In the interest of learning to fish : you can look at the Kotlin bytecode to help you answer this kind of question. In IntelliJ or Android studio, you can show it with Tools > Kotlin > Show Kotlin bytecode.

In this case, given the following code on the JVM

fun foo(l : List<Int>) : List<Int> {
  return l.filter { it in 1..100 }
}

The inner loop looks like :

    INVOKEINTERFACE java/lang/Iterable.iterator ()Ljava/util/Iterator; (itf)
    ASTORE 6
   L6
    ALOAD 6
    INVOKEINTERFACE java/util/Iterator.hasNext ()Z (itf)
    IFEQ L7
    ALOAD 6
    INVOKEINTERFACE java/util/Iterator.next ()Ljava/lang/Object; (itf)
    ASTORE 7
   L8
    ALOAD 7
    CHECKCAST java/lang/Number
    INVOKEVIRTUAL java/lang/Number.intValue ()I
    ISTORE 8
   L9
    ICONST_0
    ISTORE 9
   L10
    LINENUMBER 60 L10
    BIPUSH 100
    ICONST_1
    ILOAD 8
    ISTORE 10
    ILOAD 10
    IF_ICMPLE L11
    POP
    GOTO L12
   L11
    ILOAD 10
    IF_ICMPLT L12
    ICONST_1
    GOTO L13
   L12
    ICONST_0
   L14
   L13
    LINENUMBER 64 L13
    IFEQ L6
    ALOAD 4
    ALOAD 7
    INVOKEINTERFACE java/util/Collection.add (Ljava/lang/Object;)Z (itf)
    POP
   L15
    GOTO L6

Here you can see that the bytecode is first loading the value from the boxed int into var 8 (Number.intValue() then ISTORE 8), then pushing 100 onto the stack BIPUSH 100 and then pushing 1 ICONST 1, after which it pushes the number in var 8 twice (ILOAD 8).
That done it compares the number to 100, which are the two top stack elements IF_ICMPLE L11 (int compare lower or equal) and if the comparison succeeds it pushes the number onto the stack again and compares it to 1 IF_ICMPLT L12. If that comparison succeeds again, it will go on to call Collection.add().
This way, you can directly observe that broot was correct saying that this is compiled into x >= a && x <= b.

As a side note, on the performance side notice how this function directly invokes .iterator() and hasNext() and next() right in this function, courtesy of filter being an inline function.

1 Like