HomeAndroidChoose Operate Reference over Lambda in Kotlin? Improper!

Choose Operate Reference over Lambda in Kotlin? Improper!


Utilizing lambda as callback is quite common in Jetpack compose, however have you learnt you may as well use perform reference to interchange the lambda?

Once I first heard about this perform reference, I believed the perform reference should be higher. Not like nameless perform / lambda, it only a perform pointer or reference to a perform. It ought to run quicker and use much less reminiscence, because it would not allocate further reminiscence. So I changed lambda with perform reference each time is feasible. Effectively, I used to be improper!

After performing some researches, it seems that perform reference nonetheless allocates further reminiscence identical to what lambda does, and it additionally runs 2x slower than lambda. Earlier than we glance into why, let’s perceive their usages and variations first with examples beneath.

Lambda (name the item’s perform)

As an example you could have Display screen and ViewModel courses beneath. The id is used to determine a unique occasion of ViewModel.

class Display screen(personal val callback: () -> Unit) {
    enjoyable onClick() = callback()
}

class ViewModel(var id: String) {
    enjoyable screenOnClick() {
        println("ViewModel$id onClick() known as!")
    }

    enjoyable screenOnClickDoNothing() {}
}

In predominant(), it creates the ViewModel object. Then, you go within the lambda callback parameter – { viewModel.screenOnClick() } to Display screen‘s constructor.

enjoyable predominant() {
    var viewModel = ViewModel("1")
    val display = Display screen {
        viewModel.screenOnClick()
    }
    viewModel = ViewModel("2")
    display.onClick()
}

viewModel.screenOnClick() is the item’s perform.

To reveal the distinction between lambda and performance reference, you replace the viewModel variable with one other new occasion of ViewModelviewModel = ViewModel("2") earlier than calling display.onClick().

Calling display.onClick() is to simulate display callback

Right here is the output:

ViewModel2 onClick() known as!

Please be aware that ViewModel2 known as as an alternative of ViewModel1. It appears to be like like lambda holds the reference to the viewModel variable. When the variable is up to date, it’s mechanically mirrored.

Object’s Operate Reference

Let’s change the lambda with a perform reference – viewModel::screenOnClick. The code appears to be like like this.

enjoyable predominant() {
    var viewModel = ViewModel("1")
    val display = Display screen(callback = viewModel::screenOnClick)
    viewModel = ViewModel("2")
    display.onClick()
}

Right here is the output

ViewModel1 onClick() known as!

Please be aware that ViewModel1 known as as an alternative of ViewModel2. It appears to be like just like the perform reference is caching the ViewModel occasion when it’s known as. Thus, it’s the previous ViewModel occasion and never the up to date one. Improper!

In the event you do not change the viewModel occasion however modifying ViewModel.id immediately as an alternative,

enjoyable predominant() {
    var viewModel = ViewModel("1")
    val display = Display screen(callback = viewModel::screenOnClick)
    viewModel.id = "2"
    display.onClick()
}

you get the up to date worth.

ViewModel2 onClick() known as!

This tells you that perform reference would not cache the ViewModel occasion, however holding its reference. If the unique viewModel is changed, the Display screen nonetheless holds the previous reference of the ViewModel. Thus, it prints out the worth of that previous reference.

Efficiency Take a look at (Object’s Operate)

Let’s run some efficiency checks.

That is the lambda check that creating Display screen 1 billions instances.

enjoyable lambdaTest() {
    val viewModel = ViewModel("1")

    val timeMillis = measureTimeMillis {
        repeat(1_000_000_000) {
            val display = Display screen {
                viewModel.screenOnClickDoNothing()
            }

            display.onClick()
        }
    }
    println("lambdaTest: ${timeMillis/1000f} seconds")
}

The output present it runs ~2.4 seconds.

lambdaTest: 2.464 seconds

Now, it’s perform reference’s flip.

enjoyable funRefTest() {

    val viewModel = ViewModel("1")

    val timeMillis = measureTimeMillis {
        repeat(1_000_000_000) {
            val display = Display screen(
                callback = viewModel::screenOnClickDoNothing
            )

            display.onClick()
        }
    }
    println("funRefTest: ${timeMillis/1000f} seconds")
}

The output reveals it runs 5.2 seconds.

funRefTest: 5.225 seconds

This concludes that lambda runs ~2x quicker than perform reference. Why? Let us take a look at the decompiled code.

Decompiled Code In Java

Let us take a look at the decompiled code in Java. That is the simplified model with out the measureTimeMillis and repeat code.

lambdaTest()

public static closing void lambdaTest() {
  closing ViewModel viewModel = new ViewModel();
  Display screen display = new Display screen((Function0)(new Function0() {

     public Object invoke() {
        this.invoke();
        return Unit.INSTANCE;
     }

     public closing void invoke() {
        viewModel.screenOnClickDoNothing();
     }
  }));
  display.onClick();
}

funRefTest()

public static closing void funRefTest() {
  ViewModel viewModel = new ViewModel();
  Display screen display = new Display screen((Function0)(new Function0(viewModel) {

     public Object invoke() {
        this.invoke();
        return Unit.INSTANCE;
     }

     public closing void invoke() {
        ((ViewModel)this.receiver).screenOnClickDoNothing();
     }
  }));
  display.onClick();
}

Few necessary issues right here:

  • Operate reference allocates new perform reminiscence identical to the lambda – new Function0() and new Function0(viewModel). So it isn’t a perform pointer.
  • Operate reference passes within the viewModel into Function0 and lambda would not

Based mostly on these findings, I think the passed-in ViewModel reference might be the overhead (requires further copy to carry the reference?) that contributes to slowness in perform reference.

What about non-object’s perform?

As an example you progress screenOnClickDoNothing() out of ViewModel

enjoyable screenOnClickDoNothing() {}

and name it immediately – change viewModel.screenOnClickDoNothing() with screenOnClickDoNothing() in lambda.

enjoyable lambdaTest() {
    val timeMillis = measureTimeMillis {
        repeat(1_000_000_000) {
            val display = Display screen {
                screenOnClickDoNothing()
            }

            display.onClick()
        }
    }
    println("lambda1Test: ${timeMillis/1000f} seconds")
}

It takes 0.016 seconds to run.

lambdaTest: 0.016 seconds

What about perform reference? Exchange viewModel::screenOnClickDoNothing with ::screenOnClickDoNothing

enjoyable globalFunRefTest(){
    val timeMillis = measureTimeMillis {
        repeat(1_000_000_000) {
            val display = Display screen(callback = ::screenOnClickDoNothing)
            display.onClick()
        }
    }
    println("funRefTest: ${timeMillis/1000f} seconds")
}

and it takes the identical period of time as lambda.

funRefTest: 0.016 seconds

It makes senses as a result of each have the identical precise decompiled Java code.

public static closing void lambdaTest() {
    Display screen display = new Display screen((Function0)null.INSTANCE);
    display.onClick();
}

public static closing void funRestTest() {
    Display screen display = new Display screen((Function0)null.INSTANCE);
    display.onClick();
}

One necessary factor to notice is, though lambda is used, there is no such thing as a new perform reminiscence allotted. Perhaps the compiler is sensible sufficient to optimize it. Thus, it runs quicker than lambda with object’s perform within the earlier examples.

By the way in which, all of the checks I ran up to now is predicated on Kotlin compiler model 1.7.10.

Conclusion

There’s a slight completely different habits between utilizing lambda vs perform reference, however I do not see any sensible use case to make use of both of them. By guessing, I believe lambda ought to cowl 99% of use circumstances as a result of it would not require the item to be initialized first whereas registering the callback.

Right here is the abstract of Lambda vs Operate Reference habits:

Lambda Operate Reference
Retains the up-to-date object reference Retains the copy of the item reference
Does NOT require the item to be initialized Requires the item to be initialized

Calling the perform 1 billions instances might not be sensible, however it does show that perform reference is NOT higher than lambda in Kotlin. Virtually, it in all probability would not matter which one you employ. The distinction is probably going negligible.

Perhaps as a result of I am coming for C++/C# background, I had an impression that perform reference in Kotlin is sort of a perform pointer. You maintain the reference to the perform, and you don’t allocate new reminiscence. No, it isn’t the case in Kotlin.

Given this little or no analysis I did, I’ve determined to make use of lambda over perform reference to implement callbacks as a result of it has much less overhead. It additionally would not scale back readability an excessive amount of, in my view. Simply the additional { }?

[Updated – Sept 27, 2022]: I take again my phrase. I do discover lambda is a bit much less readable particularly the lambda has arguments whereas engaged on this undertaking.

RELATED ARTICLES

LEAVE A REPLY

Please enter your comment!
Please enter your name here

Most Popular

Recent Comments