Exploring performance overhead hidden in Kotlin -Part 3

Translation Instructions:

Original title # Exploring Kotlin’s hidden costs — Part 3

Original address:medium.com/@BladeCoder…

Original author:Christophe Beyls

Proxy properties and ranges

After Posting the first two articles in the series on performance overhead of the Kotlin programming language, I received a lot of good feedback, including from Jake Wharton himself. So if you haven’t read the first two articles, don’t miss it.

In Part 3, we’ll uncover more of the secrets of the Kotlin compiler and offer new tips on how to write more efficient code.

1. Proxy properties

A proxy property is a property whose internal implementation of its getters and optional setters can be provided by an external object of the proxy. It allows reuse of internal implementations of custom properties.

class Example {
    var p: String by Delegate()
}
Copy the code

The proxy object must implement an operator getVlue() function and a setValue() function for reading/writing properties. These functions accept metadata metadata containing the object instance and properties as additional parameters (such as its property name).

When a proxy property is declared in a class, compilation produces the following code (here is the decompiled Java code):

public final class Example {
   @NotNull
   private final Delegate p$delegate = new Delegate();
   // $FF: synthetic field
   static finalKProperty[] ? delegatedProperties =new KProperty[]{(KProperty)Reflection.mutableProperty1(new MutablePropertyReference1Impl(Reflection.getOrCreateKotlinClass(Example.class), "p"."getP()Ljava/lang/String;"))};

   @NotNull
   public final String getP(a) {
      return this.p$delegate.getValue(this, ?delegatedProperties[0]);
   }

   public final void setP(@NotNull String var1) {
      Intrinsics.checkParameterIsNotNull(var1, "
      
       "
      ?>);
      this.p$delegate.setValue(this, ?delegatedProperties[0], var1); }}Copy the code

Some static attribute metadata metadata is added to the class. The proxy is initialized in the constructor of the class and then invoked each time a property is read or written.

Proxy instance

In the example above, a new instance of the proxy object will be created to implement this property. This is required when the proxy instance is stateful, such as when calculating the value of a local cache attribute.

class StringDelegate {
    private var cache: String? = null

    operator fun getValue(thisRef: Any? , property:KProperty< * >): String {
        var result = cache
        if (result == null) {
            result = someOperation()
            cache = result
        }
        return result
    }
}
Copy the code

If additional arguments passed through its constructor are also needed, a new instance of the proxy needs to be created:

class Example {
    private val nameView by BindViewDelegate<TextView>(R.id.name)
}
Copy the code

In some cases, however, only one proxy instance is required to implement arbitrary properties: when the proxy instance is stateless, and the only variables it needs to execute are the object instance and property name (which the compiler provides directly). In this case, you can make the proxy instance a singleton by declaring it as an Object object expression rather than a class.

For example, the following proxy singleton retrieves its tag name to match the name of an attribute in the Android Activity to match the Fragment.

object FragmentDelegate {
    operator fun getValue(thisRef: Activity, property: KProperty< * >): Fragment? {
        return thisRef.fragmentManager.findFragmentByTag(property.name)
    }
}
Copy the code

Similarly, any object can be extended to be a proxy. In addition, getValue() and setValue() can be declared as extension functions. Built-in extension functions are already provided in Kotlin, such as allowing Map and MutableMap instances to be proxy instances and the names of properties to be keys.

If you choose to implement multiple properties in the same class to reuse the same local proxy instance, you need to initialize the instance in the constructor of the class.

Note: starting with Kotlin1.1, it is also possible to declare local variables as proxy properties in functions. In this case, the proxy instance can delay initialization until variables are declared in the function.

Each proxy property declared in a class is involvedThe performance overhead of its associated proxy object creationAnd add some metadata metadata to the class. If necessary, try for different attributesreuseThe same proxy instance. When you declare a large number of proxy attributes, you also need to consider whether proxy attributes are your best choice.

Generic agent

Proxy functions can also be declared in a generic manner, so the same proxy class can use any attribute type.

private var maxDelay: Long by SharedPreferencesDelegate<Long> ()Copy the code

However, if you use a generic proxy with a native type attribute, as in the example above, even if the declared native type is non-NULL, you can’t avoid boxing and unboxing every time you read or write that attribute.

For a proxy property of a non-NULL native type, it is better to create a specific proxy class for that particular value type rather than a generic proxy to avoid the boxing overhead incurred each time the property is accessed

Library agent: lazy()

Kotlin has some library delegate functions built in to cover common conditions such as Delegates.notnull (),Delegates.Observable () and lazy().

Lazy (Initializer: () -> T) is a function that returns a proxy object for a read-only property that is initialized by executing the lazy function argument lambda Initializer when it is first read.

private val dateFormat: DateFormat by lazy {
    SimpleDateFormat("dd-MM-yyyy", Locale.getDefault())
}
Copy the code

This is a clever way to defer expensive initialization operations until they are actually needed, which can improve performance while preserving code readability.

Note that the lazy() Function is not inline, and the lambda passed as an argument will compile into separate Function classes and will not be inlined within the returned proxy object.

Another overloaded function that is usually ignored is lazy() actually hides an optional schema argument to determine which of three different types of proxies should be returned:

public fun <T> lazy(initializer: () -> T): Lazy<T> = SynchronizedLazyImpl(initializer)
public fun <T> lazy(mode: LazyThreadSafetyMode, initializer: () -> T): Lazy<T> =
        when (mode) {
            LazyThreadSafetyMode.SYNCHRONIZED -> SynchronizedLazyImpl(initializer)
            LazyThreadSafetyMode.PUBLICATION -> SafePublicationLazyImpl(initializer)
            LazyThreadSafetyMode.NONE -> UnsafeLazyImpl(initializer)
        }
Copy the code

. The default mode is LazyThreadSafetyMode SYNCHRONIZED will perform inspection of relatively expensive double lock, this is to ensure that when reading a property in a multithreaded environment initialization block can run safety.

If you know the current environment is a single-threaded access properties (such as the main thread), so can be used by explicitly LazyThreadSafetyMode. NONE to completely avoid double lock check the expensive cost.

val dateFormat: DateFormat by lazy(LazyThreadSafetyMode.NONE) {
    SimpleDateFormat("dd-MM-yyyy", Locale.getDefault())
}
Copy the code

uselazy()Agents can delay expensive initialization on demand, and they can specify thread-safe modes to avoid unnecessary double-lock checking.

Ii. Ranges

An interval is a special expression for representing a finite set of values in Kotlin. These values can be of any Comparable type. These expressions are formed by creating functions that implement the ClosedRange object. The main function used to create an interval is.. Operators.

Tests for interval inclusion

The main purpose of interval expressions is to use in and! The in operator to determine whether a value is included

if (i in 1.10.) {
    println(i)
}
Copy the code

This implementation is specifically optimized for non-NULL primitives (Int, Long, Byte, Short, Float, Double, or Char), so the example above can be efficiently compiled as follows:

if(1 <= i && i <= 10) {
   System.out.println(i);
}
Copy the code

The performance overhead is almost zero and there is no additional object allocation. Ranges can also be used with any other non-native Comparable types.

if (name in "Alfred"."Alicia") {
    println(name)
}
Copy the code

Prior to Kotlin 1.1.50, a temporary ClosedRange object was always created when the above example was compiled. But since 1.1.50, its implementation has been optimized to avoid the Comparable type of additional overhead allocation:

if(name.compareTo("Alfred") > =0) {
   if(name.compareTo("Alicia") < =0) { System.out.println(name); }}Copy the code

In addition, interval checking involves applying the when expression

val message = when (statusCode) {
    in 200.299. -> "OK"
    in 300.399. -> "Find it somewhere else"
    else -> "Oops"
}
Copy the code

This makes the code better than a series of if {… } else if {… } statements are more readable and more efficient.

However, in interval inclusion checking, there is a small performance overhead when there is at least one indirect procedure between the declarations of the interval. Take this Kotlin code for example:

private val myRange get() = 1.10.

fun rangeTest(i: Int) {
    if (i in myRange) {
        println(i)
    }
}
Copy the code

The above code causes an additional IntRange object to be created after compilation:

private final IntRange getMyRange(a) {
   return new IntRange(1.10);
}

public final void rangeTest(int i) {
   if(this.getMyRange().contains(i)) { System.out.println(i); }}Copy the code

Even declaring the property getter as an inline function does not avoid creating an IntRange object. In this case, the Kotlin 1.1 compiler has been improved. Because these specific interval classes exist, at least when comparing native types, no boxing occurs.

Try to use direct declaration of intervals in procedure interval checking without indirect declaration to avoid the creation and allocation of additional interval objects. In addition, they can be declared asconstantSo we can use them again.

Iteration: for loop

An interval of integer type (any interval of primitive type other than Float or Double) is also a series: it can be iterated over. This allows you to replace classic Java for loops with shorter syntax.

for (i in 1.10.) {
    println(i)
}
Copy the code

This can compile to comparable optimized code with zero overhead:

int i = 1;
for(byte var2 = 11; i < var2; ++i) {
   System.out.println(i);
}
Copy the code

If iterating backwards, use the downTo() infix function instead

for (i in 10 downTo 1) {
    println(i)
}
Copy the code

Again, compiling with this construct has zero overhead:

int i = 10;
byte var1 = 1;
while(true) {
   System.out.println(i);
   if(i == var1) {
      return;
   }
   --i;
}
Copy the code

There is also a useful until () infix function that iterates up to, but not including, an interval upper limit.

for (i in 0 until size) {
    println(i)
}
Copy the code

When the original version of this article was released, this function was called to generate suboptimal code. Since Kotlin 1.1.4, the situation has improved considerably, and the compiler now generates the equivalent Java for loop:

int i = 0;
for(int var2 = size; i < var2; ++i) {
   System.out.println(i);
}
Copy the code

However, other iterations are not optimized as well.

This is another way to use reversed() functions in combination with intervals to iterate backwards and produce exactly the same result as downTo().

for (i in (1.10.).reversed()) {
    println(i)
}
Copy the code

Unfortunately, the generated compiled code is not so pretty:

IntProgression var10000 = RangesKt.reversed((IntProgression)(new IntRange(1.10)));
int i = var10000.getFirst();
int var3 = var10000.getLast();
int var4 = var10000.getStep();
if(var4 > 0) {
   if(i > var3) {
      return; }}else if(i < var3) {
   return;
}

while(true) {
   System.out.println(i);
   if(i == var3) {
      return;
   }

   i += var4;
}
Copy the code

A temporary IntRange object will be created to represent the interval, and another IntProgression object will be created to reverse the value of the first object.

In fact, any combination of the above features for creating a Living object will generate similar code, involving the small overhead of creating at least two lightweight Living objects.

This rule also applies to using the step() infix function to modify Progression, even if the step size is 1:

for (i in 1.10. step 2) {
    println(i)
}
Copy the code

As an added note, when the generated code reads the last property in IntProgression, this performs a small amount of calculation to determine the exact final value of the interval by considering boundaries and step sizes. In the example above, the last value should be 9.

To iterate through a for loop, it is best to use interval expressions, which involve only pairs.ordownTo()oruntill()To avoid the overhead of creating temporary Living objects.

Iterations: for-each ()

Instead of using the for loop, try using the forEach () inline extension function on the interval to achieve the same result.

(1.10.).forEach {
    println(it)
}
Copy the code

However, if you look closely at the signature of the forEach () function used here, you will notice that it is not optimized for ranges, but only for Iterable, so an iterator needs to be created. This is the decompiled Java code representation:

Iterable $receiver$iv = (Iterable)(new IntRange(1.10));
Iterator var1 = $receiver$iv.iterator();

while(var1.hasNext()) {
   int element$iv = ((IntIterator)var1).nextInt();
   System.out.println(element$iv);
}
Copy the code

This code is even less efficient than the previous example, because in addition to creating an IntRange object, you must also have the overhead of creating an IntIterator. At the very least, this generates a value of a primitive type.

To iterate over a range, it is better to use a simple for loop rather than call forEach () on it to avoid the overhead of the iterator object.

Iteration: Collection indices

The Kotlin library provides built-in index extension properties to generate ranges of array and Collection indexes.

val list = listOf("A"."B"."C")
for (i in list.indices) {
    println(list[i])
}
Copy the code

Surprisingly, the code traversing indices is also compiled as optimized code:

List list = CollectionsKt.listOf(new String[]{"A"."B"."C"});
int i = 0;
for(int var2 = ((Collection)list).size(); i < var2; ++i) {
   Object var3 = list.get(i);
   System.out.println(var3);
}
Copy the code

Here, we can see that no IntRange object is created at all and that list iteration is as efficient as possible.

This works well for implementing arrays and classes of collections, so you might define your own Indices extensions in your own defined classes and expect the same iterative performance.

inline val SparseArray<*>.indices: IntRange
    get() = 0 until size()

fun printValues(map: SparseArray<String>) {
    for (i in map.indices) {
        println(map.valueAt(i))
    }
}
Copy the code

However, after compiling, we can see that it is not efficient because the compiler cannot intelligently avoid creating interval objects:

public static final void printValues(@NotNull SparseArray map) {
   Intrinsics.checkParameterIsNotNull(map, "map");
   IntRange var10000 = RangesKt.until(0, map.size());
   int i = var10000.getFirst();
   int var2 = var10000.getLast();
   if(i <= var2) {
      while(true) {
         Object $receiver$iv = map.valueAt(i);
         System.out.println($receiver$iv);
         if(i == var2) {
            break; } ++i; }}}Copy the code

Instead, I recommend using until() directly in the for loop

fun printValues(map: SparseArray<String>) {
    for (i in 0 until map.size()) {
        println(map.valueAt(i))
    }
}
Copy the code

When traversal is not implementedCollectionOf the interfaceCustom collectionWhen,It is best to write your own index scope directly in the for loopInstead of relying on functions or attributes to generate intervals, to avoid assigning interval objects.

I hope this is as much fun for you to read as it is for me to write. You’ll probably see more of this in the future, but the first three sections cover everything I planned to write initially. If you like it, please share it with others, thanks!

conclusion

This is the end of a series of articles exploring the performance overhead of Kotlin. Translating this series has helped me write more efficient and better code when I develop with Kotlin. So I felt the need to translate it and share it with you. Next stop, we’ll enter the Kotlin coroutine

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 the hidden performance overhead in Kotlin -Part 2
  • 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