Translation Instructions:

Exploring Kotlin’s hidden costs — Part 2

Original address:medium.com/@BladeCoder…

Original author:Christophe Beyls

This is part 2 on exploring the performance overhead hidden in Kotlin, and don’t forget to read Part 1 if you haven’t seen it yet.

Let’s explore and discover more details about Kotlin’s syntax implementation from the ground up.

Local function

This is a function that we didn’t cover in the first article: you declare the function inside the body of another function just as you would normally define an ordinary function. These are called local functions, and they can access the scope of an external function.

fun someMath(a: Int): Int {
    fun sumSquare(b: Int) = (a + b) * (a + b)

    return sumSquare(1) + sumSquare(2)}Copy the code

Let’s start with the biggest limitations of local functions. Local functions cannot be declared inline and functions that contain local functions cannot be declared inline. There is no effective way to avoid the overhead of function calls in this case.

When compiled, these local functions are converted to Function objects, just like lambda expressions, with the same limitations as in part1 of the previous article regarding non-inline functions. Decompiled Java code:

public static final int someMath(final int a) {
   Function1 sumSquare$ = new Function1(1) {
      // $FF: synthetic method
      // $FF: bridge method
      // Note: This is the invoke generic synthesis method generated by the Function1 interface
      public Object invoke(Object var1) {
         return Integer.valueOf(this.invoke(((Number)var1).intValue()));
      }

      // Note: invoke is the instance's specific method
      public final int invoke(int b) {
         return(a + b) * (a + b); }};return sumSquare$.invoke(1) + sumSquare$.invoke(2);
}
Copy the code

But compared to lambda expressions, it has a much smaller impact on performance: Since the instance object of the Function is known from the caller, it will call invoke directly from that instance’s specific method rather than invoke directly from the Function interface. This means that local functions are called from external functions without primitive type conversions or boxing operations. We can verify this by looking at the bytecode:

   ALOAD 1
   ICONST_1
   INVOKEVIRTUAL be/myapplication/MyClassKt$someMathThe $1.invoke    (I)I
   ALOAD 1
   ICONST_2
   INVOKEVIRTUAL be/myapplication/MyClassKt$someMathThe $1.invoke    (I)I
   IADD //加法操作
   IRETURN
Copy the code

We can see that the function that is called twice takes an Int and returns an Int, and that the addition is performed immediately, without any intermediate boxing or unboxing.

Of course, a new Function object is still created each time the method is called. But this can be avoided by writing the local function in a non-capture way:

fun someMath(a: Int): Int {
    fun sumSquare(a: Int, b: Int) = (a + b) * (a + b)

    return sumSquare(a, 1) + sumSquare(a, 2)}Copy the code

Now the same Function instance will be reused, again without forced conversions or boxing. The only disadvantage of this local function over a normal private function is that it uses some methods to generate additional classes.

Local functions are substitutes for private functions with the added benefit of being able to access local variables of external functions. However, this benefit comes with the implicit cost of creating a Function object for each call to an external Function, so non-captured local functions are preferred.

Air safety

One of the best features of the Kotlin language is that it draws a clear distinction between nullable and non-nullable types. This allows compilation to effectively prevent accidental NullPointerexceptions by disallowing any code that assigns a non-NULL or nullable value to a non-NULL variable at runtime.

Run-time checking of non-null parameters

Let’s declare a public function that uses a non-null string as an admissible number:

fun sayHello(who: String) {
    println("Hello $who")}Copy the code

Now look at the corresponding decompiled Java code:

public static final void sayHello(@NotNull String who) {
   Intrinsics.checkParameterIsNotNull(who, "who");// Execute static functions for non-null checks
   String var1 = "Hello " + who;
   System.out.println(var1);
}
Copy the code

Note that the Kotlin compiler is very Java friendly, and you can see that the @notnull annotation is automatically added to function arguments, so Java tools can use this annotation to display warnings when passing null values.

However, annotations are not sufficient to force external callers to pass non-NULL values. As a result, the compiler is still at the beginning of a function to add a static method calls, this method will check parameter, if it is null, throwing an IllegalArgumentException. To make unsafe caller code easier to fix, the function throws exceptions early and consistently, rather than leaving it behind to throw run-time NullPointerExceptions.

In fact, every function of the public has a on the Intrinsics. CheckParameterIsNotNull () static call, the call for each non-null reference parameters. These checks are not added to private functions because the compiler guarantees null-safe code in the Kotlin class.

These static calls have a negligible impact on performance and can be very helpful when debugging and testing your application. Having said that, you might think this is an unnecessary overhead for a release. In this case, you can disable run-time null checking using the -xno-param-assertions compiler option or by adding the following Proguard rule:

-assumenosideeffects class kotlin.jvm.internal.Intrinsics {
    static void checkParameterIsNotNull(java.lang.Object, java.lang.String);
}
Copy the code

Nullable native type

One thing seems to be well known, but a word of caution: nullable types are always reference types. Declaring variables of primitive types as nullable types prevents Kotlin from using Java primitive data types (such as int or float) instead of boxed reference types (such as Integer or float), which avoids the additional overhead of boxing and unboxing operations.

Java, on the other hand, allows you to use Integer variables that almost look like int variables, thanks to auto-boxing and ignoring null security, whereas Kotlin forces you to write null-safe code when using nullable types, so the benefits of using non-NULL types become more obvious:

fun add(a: Int, b: Int): Int {
    return a + b
}
fun add(a: Int? , b:Int?).: Int {
    return(a ? :0) + (b ? :0)}Copy the code

Use non-NULL native types whenever possible to improve code readability and performance.

About array

There are three types of arrays in Kotlin:

  • IntArray,FloatArray, and other arrays of primitive types. This is eventually compiled into arrays of int[],float[], and other basic data types

  • Array

    : an Array that is not an empty object reference type

  • Array

    : array of nullable object reference types Obviously, there is also a native type boxing involved here
    ?>

If you need an array of non-null primitives, use itIntArrayRather thanArray<Int>To avoid the performance overhead of the boxing process

Variable number of parameters (Varargs)

Like Java, Kotlin allows functions to be declared with a variable number of arguments. It’s just that the declaration syntax is a little different:

fun printDouble(vararg values: Int) {
    values.forEach { println(it * 2)}}Copy the code

Just as in Java, the vararg parameter is actually compiled as an array parameter of the given type. These functions can then be called in three different ways:

1. Pass multiple parameters

printDouble(1.2.3)
Copy the code

The Kotlin compiler will translate this code into new array creation and initialization, just like the Java compiler does:

printDouble(new int[] {1.2.3});
Copy the code

So creating a new array incurs overhead, but this is nothing new compared to Java.

2. Pass a single array

The difference here is that in Java, you can pass an existing array reference directly as a vararg parameter. In Kotlin, we use the spread operator:

val values = intArrayOf(1.2.3)
printDouble(*values)
Copy the code

In Java, array references are passed to functions as is, without allocating additional array space. However, as you can see in decompiled Java code, the Kotlin stretch (spread) operator is compiled differently:

int[] values = new int[]{1.2.3};
printDouble(Arrays.copyOf(values, values.length));
Copy the code

When a function is called, an existing array is always copied. The benefit is that the code is safer: it allows the function to modify the array without affecting the caller code. But it will allocate extra memory.

Note that calling a Java method with a variable number of arguments in the Kotlin code has the same effect.

3. Pass a mixture of arrays and arguments

The main benefit of the Kotlin stretch (spread) operator is that it also allows arrays to be mixed with other parameters in the same call.

val values = intArrayOf(1.2.3)
printDouble(0, *values, 42)
Copy the code

How will the above code compile? Generating code would be interesting:

int[] values = new int[] {1.2.3};
IntSpreadBuilder var10000 = new IntSpreadBuilder(3);
var10000.add(0);
var10000.addSpread(values);
var10000.add(42);
printDouble(var10000.toArray());
Copy the code

In addition to creating a new array, a temporary generator object is used to calculate the final array size and populate it. This adds another small overhead to the method call.

Even when using values from an existing array, calling a function with a variable number of arguments in Kotlin increases the cost of creating a new temporary array. For code that repeatedly calls this function that is critical for performance, consider adding an actual array parameter insteadvarargThe method of

Thank you for reading, and if you like, please share this article.

Continue reading in Part 3: Delegate properties and scope.

Readers have something to say

A long, long time ago, I wrote Part1 of a series exploring performance overhead hidden in Kotlin. Read article 1 if you haven’t, because this series will really help you write efficient Kotlin code, and will help you understand the principles behind Kotlin syntax at the source and compile level. I prefer to call these Kotlin coding techniques Effective Kotlin, which is why I originally translated this series of articles. There are a few things I need to add to this article:

1. Why do non-capturing local functions reduce overhead

In fact, the concepts of capture and non-capture have been mentioned in previous articles, such as variable capture, lambda capture and non-capture.

Using the above local functions as an example, let’s compare the two functions:

// Capture local functions before overwriting
fun someMath(a: Int): Int {
    fun sumSquare(b: Int) = (a + b) * (a + b)// The local function refers to the external function directly.
    SumSquare is called capturing local functions because local functions can access the scope of external functions

    return sumSquare(1) + sumSquare(2)}// Decompile code before overwrite
 public static final int someMath(final int a) {
      // Create Function1 object $fun$sumSquare$1, so a Function1 object is created every time someMath is called
      <undefinedtype> $fun$sumSquare$1 = new Function1(a) {
         // $FF: synthetic method
         // $FF: bridge method
         public Object invoke(Object var1) {
            return this.invoke(((Number)var1).intValue());
         }

         public final int invoke(int b) {
            return(a + b) * (a + b); }};return $fun$sumSquare$1.invoke(1) + $fun$sumSquare$1.invoke(2);
   }
Copy the code

Capturing local functions generates additional Function objects, so we try to use non-capturing local functions to reduce performance overhead.

// A rewritten non-capturing local function
fun someMath(a: Int): Int {
    // The local function is not captured. The local function is not captured
    fun sumSquare(a: Int, b: Int) = (a + b) * (a + b)
    return sumSquare(a,1) + sumSquare(a,2)}// Decompile the code after rewriting
public static final int someMath(int a) {
    // Note: You can see that a non-captured local function instance is a singleton, and multiple calls will only reuse the previous instance and not recreate it.
    <undefinedtype> $fun$sumSquare$1 = null.INSTANCE;
    return $fun$sumSquare$1.invoke(a, 1) $fun$sumSquare$1.invoke(a, 2);
}
Copy the code

From this comparison, it should be clear what is captured and what is not captured, and why non-captured local functions reduce performance overhead.

2. Summarize the cost of improving Kotlin code performance

  • Local functions are substitutes for private functions with the added benefit of being able to access local variables of external functions. However, this benefit comes with the implicit cost of creating a Function object for each call to an external Function, so non-captured local functions are preferred.
  • For release apps, especially Android apps, you can use it-Xno-param-assertionsCompiler option or add the following Proguard rule to disable null checking at runtime:
-assumenosideeffects class kotlin.jvm.internal.Intrinsics {
    static void checkParameterIsNotNull(java.lang.Object, java.lang.String);
}
Copy the code
  • This is best used when you need an array of non-NULL primitive typesIntArrayRather thanArray<Int>To avoid the performance overhead of the boxing process

The last

First of all, I would like to say sorry to those who have been following my official account and technical blog. It has been a long time since I updated the technical articles, so many of them have left, but some of them have been quietly supporting. So I am ready to update the article again from today. I have been studying DART and flutter for some time and accumulated some technical knowledge, so I will update some articles about DART and flutter irregularly. Thank you for your attention and understanding.

Welcome to the Kotlin Developer Association, where the latest Kotlin technical articles are published, and a weekly Kotlin foreign technical article is translated from time to time. If you like Kotlin, welcome to join us ~~~

Welcome to Kotlin’s series of articles:

Kotlin Encounter Design Pattern collection:

  • When Kotlin Meets Design Patterns perfectly singleton Patterns (PART 1)

Data Structure and Algorithm series:

  • Binary Search for algorithms every Week (described by Kotlin)
  • Weekly Linked List of Data structures (Kotlin description)

Translation series:

  • Explore performance overhead hidden in Kotlin -Part 1
  • What Kotlin said about the Companion Object
  • Remember a Kotlin official document translation PR(inline type)
  • Exploration of autoboxing and High performance for inline classes in Kotlin (ii)
  • Kotlin inline class full resolution
  • Kotlin’s trick of Reified Type Parameter
  • When should type parameter constraints be used in Kotlin generics?
  • An easy way to remember Kotlin’s parameters and arguments
  • Should Kotlin define functions or attributes?
  • How to remove all of them from your Kotlin code! (Non-empty assertion)
  • Master Kotlin’s standard library functions: run, with, let, also, and apply
  • All you need to know about Kotlin type aliases
  • Should Sequences or Lists be used in Kotlin?
  • Kotlin’s turtle (List) rabbit (Sequence) race

Original series:

  • How do you fully parse annotations in Kotlin
  • How do you fully parse the type system in Kotlin
  • How do you make your callbacks more Kotlin
  • Jetbrains developer briefing (3) Kotlin1.3 new features (inline class)
  • JetBrains developer briefing (2) new features of Kotlin1.3 (Contract and coroutine)
  • Kotlin/Native: JetBrains Developer’s Day (Part 1
  • How to overcome the difficulties of generic typing in Kotlin
  • How to overcome the difficulties of Generic typing in Kotlin (Part 2)
  • How to overcome the difficulties of Generic typing in Kotlin (Part 1)
  • Kotlin’s trick of Reified Type Parameter (Part 2)
  • Everything you need to know about the Kotlin property broker
  • Source code parsing for Kotlin Sequences
  • Complete analysis of Sets and functional apis in Kotlin – Part 1
  • Complete parsing of lambdas compiled into bytecode in Kotlin syntax
  • On the complete resolution of Lambda expressions in Kotlin’s Grammar
  • On extension functions in Kotlin’s Grammar
  • A brief introduction to Kotlin’s Grammar article on top-level functions, infix calls, and destruct declarations
  • How to Make functions call Better
  • On variables and Constants in Kotlin’s Grammar
  • Elementary Grammar in Kotlin’s Grammar Essay

Translated by Effective Kotlin

  • The Effective Kotlin series considers arrays of primitive types for optimal performance (5)
  • The Effective Kotlin series uses Sequence to optimize set operations (4)
  • Exploring inline modifiers in higher-order functions (3) Effective Kotlin series
  • Consider using a builder when encountering multiple constructor parameters in the Effective Kotlin series.
  • The Effective Kotlin series considers using static factory methods instead of constructors (1)

Actual combat series:

  • Use Kotlin to compress images with ImageSlimming.
  • Use Kotlin to create a picture compression plugin.
  • Use Kotlin to compress images.
  • Simple application of custom View picture fillet in Kotlin practice article