Extention functions performances


#1

I’m quite addicted to extensions functions. it’s an awesome tool :slight_smile:
I’m wondering how well it perform. I don’t know if it’s a slightly performance lost or no lost at all.

Some example that I have used :

//1
override fun getParams(): MutableList<String> = super.getParams().also {
    it += "someParams"
}

//2 
val filteredList = myList.filter { /* predicate */ }.takeIf { it.isNotEmpty() }
     ?: run { /* some error handling */ ; return }

For algorithms similar to (2), does a sequence will be faster ? Like so :

val filteredList = mySequence.filter { /* predicate */ }.toList().takeIf { it.isNotEmpty() } 
    ?: run { /* some error handling */; return }

#2

I’m not entirely sure what you want to know. If I understand you right you have 2 questions.

  1. “Do extension functions have a performance loss?”
  2. “Are sequences faster than lists in the example above?”

So regarding the first question: No they don’t. Let’s take this extension function

fun Int.add5() : Int  = this + 5

// usage site
val foo = 5
println(foo.add5()) // 10

This would be compiled down to following code:

val foo = 5
fun add5(receiver: Int) = receiver + 5

// usage site
val foo = 5
println(add5(foo)) // 10

I am no expert on how the compiler works, but basically every extension function gets converted into a function which takes the receiver as the first argument. So no there is no performance loss there.

As to your second question. I have not tested this but in your example there should not be a difference between list and sequence. Sequence has some optimizations build into it using lazy execution so that each value is only calculated once you need it. That way you can iterate a sequence before some heavy calculation is computed for each element. Take for example

val squares = List(1000) { it * it } // list of the first 1000 square numbers

squares.map { math.sqrt(it) }.forEach { println(it) }

This would first calculate the square root of each number and than once all roots are calculated print all of them at once.

val squares = List(1000) {it * it } 

squares.asSequence().map{ math.sqrt(it) }.forEach {println(it) }

This would in contrast first calculate the first sqaure root and than print it. Only after printing it, the next square root would be calculated.
In the end both versions would take about the same time, but in one case you see the results once they are calculated and in the other you would need to wait for the entire calculation to be finished, before you get the results displayed. https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.sequences/


#3

Thanks for information :grinning:


#4

Extensions functions are usual static methods under the hood.


#5

In addition to the sequence:

With lists, you make every element perform an action and store them in a new list, whereafter you do another action and again store them in a list.

statefull / stateless

Every operation in a Sequence can be labeled as either statefull or stateless:

  • Stateless means that it looks only at one or a couple of elements: eg. map, filter.
  • Statefull meanst that it looks at all the elements: eg. foreach, sorted.

If you don’t use statefull operations, a lot of elements will never be evaluated.
You can use peek to see it very well, because it executes an operation on every element that passes by:

sequenceOf(0, 1, 2, 3, 4, 5).map{ it + 1 }.peek{ print(it) }.first()  //prints 1
sequenceOf(0, 1, 2, 3, 4, 5).map{ it + 1 }.peek{print(it) }.forEach{} //prints 12345

terminal/intermediate

We can go even further, as you have two kinds of operations:

  • terminal: Do something with the elements in the sequence: eg. foreach, first, toList
  • intermediate: perform an action with the sequence itself: eg. map, filter, peek

When you never call a terminal operation, none of the elements will do anything:

    sequenceOf(0, 1, 2, 3, 4, 5).map{ it + 1 }.peek{ print(it) } //doens't print anything
    sequenceOf(0, 1, 2, 3, 4, 5).map{ it + 1 }.peek{print(it) }.sorted() //doesn't print anything

parallel

To run in parallel, you have a couple of options:

  1. you can use java’s streams
    sequenceOf(0, 1, 2, 3, 4, 5).asStream().parallel().map{it+1}.asSequence()

  2. you can create your own implementation (probably worser than the real one, but it was fun to figure out):

    fun <S, T> Sequence<S>.map(groupSize: Int, transform: (S) -> T) = buildSequence {
       val threads = Executors.newCachedThreadPool()
       chunked(groupSize).forEach {
           it.map{
               threads.submit(Callable { transform(it) })
           }.forEach {
               yield(it.get() as T)
           }
       }
    }
    
  3. my recommendation: use channels. This makes use of coroutines.