Kotlin, компиляция в байткод и производительность (часть 2)



    Это продолжение публикации. Первую часть можно посмотреть тут

    Содержание:


    Циклы
    When
    Делегаты
    Object и companion object
    lateinit свойства
    coroutines
    Выводы

    Циклы:


    В языке Kotlin отсутствует классический for с тремя частями, как в Java. Кому-то это может показаться проблемой, но если подробнее посмотреть все случаи использования такого цикла, то можно увидеть, что по большей части он применяется как раз для перебора значений. На смену ему в Kotlin есть упрощенная конструкция.

    //Kotlin
    fun rangeLoop() {
        for (i in 1..10) {
            println(i)
        }
    }
    

    1..10 тут это диапазон по которому происходит итерация. Компилятор Kotlin достаточно умный, он понимает что мы собираемся в данном случае делать и поэтому убирает весь лишний оверхед. Код компилируется в обычный цикл while с переменной счетчика цикла. Никаких итераторов, никакого оверхеда, все достаточно компактно.

    //Java
    public static final void rangeLoop() {
          int i = 1;
          byte var1 = 10;
          if(i <= var1) {
             while(true) {
                System.out.println(i);
                if(i == var1) {
                   break;
                }
     
                ++i;
             }
          }
     
     }
    

    Похожий цикл по массиву (который в Kotlin записывается в виде Array<*>), компилируется аналогичным образом в цикл for.

    //Kotlin
    fun arrayLoop(x: Array<String>) {
        for (s in x) {
            println(s)
        }
    }
    

    //Java
    public static final void arrayLoop(@NotNull String[] x) {
          Intrinsics.checkParameterIsNotNull(x, "x");
     
          for(int var2 = 0; var2 < x.length; ++var2) {
             String s = x[var2];
             System.out.println(s);
          }
     
     }
    

    Немного другая ситуация возникает, когда происходит перебор элементов из списка:

    //Kotlin
    fun listLoop(x: List<String>) {
        for (s in x) {
            println(s)
        }
    }
    

    В этом случае приходится использовать итератор:

    //Java
    public static final void listLoop(@NotNull List x) {
          Intrinsics.checkParameterIsNotNull(x, "x");
          Iterator var2 = x.iterator();
     
          while(var2.hasNext()) {
             String s = (String)var2.next();
             System.out.println(s);
          }
     
     }
    

    Таким образом, в зависимости от того по каким элементам происходит перебор, компилятор Kotlin сам выбирает самый эффективный способ преобразовать цикл в байткод.

    Ниже приведено сравнение производительности для циклов с аналогичными решениями в Java:

    Циклы




    Как видно разница между Kotlin и Java минимальна. Байткод получается очень близким к тому что генерирует javac. По словам разработчиков они еще планируют улучшить это в следующих версиях Kotlin, чтобы результирующий байткод был максимально близок к тем паттернам, которые генерирует javac.

    When


    When — это аналог switch из Java, только с большей функциональностью. Рассмотрим ниже несколько примеров и то, во что они компилируются:

    /Kotlin
    fun tableWhen(x: Int): String = when(x) {
        0 -> "zero"
        1 -> "one"
        else -> "many"
    }
    

    Для такого простого случая результирующий код компилируется в обычный switch, тут никакой магии не происходит:

    //Java
    public static final String tableWhen(int x) {
          String var10000;
          switch(x) {
          case 0:
             var10000 = "zero";
             break;
          case 1:
             var10000 = "one";
             break;
          default:
             var10000 = "many";
          }
     
          return var10000;
    }
    

    Если же немного изменить пример выше, и добавить константы:

    //Kotlin 
    val ZERO = 1
    val ONE = 1
     
    fun constWhen(x: Int): String = when(x) {
        ZERO -> "zero"
        ONE -> "one"
        else -> "many"
    }
    

    То код в этом случае уже компилируется в следующий вид:

    //Java
    public static final String constWhen(int x) {
          return x == ZERO?"zero":(x == ONE?"one":"many");
    }
    

    Это происходит потому, что на данный момент компилятор Kotlin не понимает, что значения являются константами, и вместо преобразования к switch, код преобразуется к набору сравнений. Поэтому вместо константного времени происходит переход к линейному (в зависимости от количества сравнений). По словам разработчиков языка, в будущем это может быть легко исправлено, но в текущей версии это пока так.

    Существует также возможность использовать модификатор const для констант, известных на момент компиляции.
    //Kotlin (файл When2.kt)
    const val ZERO = 1
    const val ONE = 1
     
    fun constWhen(x: Int): String = when(x) {
        ZERO -> "zero"
        ONE -> "one"
        else -> "many"
    }
    

    Тогда в этом случае компилятор уже правильно оптимизирует when:
    public final class When2Kt {
       public static final int ZERO = 1;
       public static final int ONE = 2;
    
       @NotNull
       public static final String constWhen(int x) {
          String var10000;
          switch(x) {
          case 1:
             var10000 = "zero";
             break;
          case 2:
             var10000 = "one";
             break;
          default:
             var10000 = "many";
          }
    
          return var10000;
       }
    }
    

    Если же заменить константы на Enum:

    //Kotlin (файл When3.kt)
    enum class NumberValue {
        ZERO, ONE, MANY
    }
     
    fun enumWhen(x: NumberValue): String = when(x) {
        NumberValue.ZERO -> "zero"
        NumberValue.ONE -> "one"
        NumberValue.MANY -> "many"
    }
    

    То код, также как в первом случае, будет компилироваться в switch (практический такой же как в случае перебора enum в Java).

    //Java
    public final class When3Kt$WhenMappings {
       // $FF: synthetic field
       public static final int[] $EnumSwitchMapping$0 = new int[NumberValue.values().length];
     
       static {
          $EnumSwitchMapping$0[NumberValue.ZERO.ordinal()] = 1;
          $EnumSwitchMapping$0[NumberValue.ONE.ordinal()] = 2;
          $EnumSwitchMapping$0[NumberValue.MANY.ordinal()] = 3;
       }
    }
    
    
    public static final String enumWhen(@NotNull NumberValue x) {
          Intrinsics.checkParameterIsNotNull(x, "x");
          String var10000;
          switch(When3Kt$WhenMappings.$EnumSwitchMapping$0[x.ordinal()]) {
          case 1:
             var10000 = "zero";
             break;
          case 2:
             var10000 = "one";
             break;
          case 3:
             var10000 = "many";
             break;
          default:
             throw new NoWhenBranchMatchedException();
          }
     
          return var10000;
    }
    

    По ordinal номеру элемента определяется номер ветки в switch, по которому далее и происходит выбор нужной ветви.

    Посмотрим на сравнение производительности решений на Kotlin и Java:

    When




    Как видно простой switch работает точно также. В случае, когда компилятор Kotlin не смог определить что переменные константы и перешел к сравнениям, Java работает чуть быстрее. И в ситуации, когда перебираем значения enum, также есть небольшая потеря на возню с определением ветви по значению ordinal. Но все эти недостатки будут исправлены в будущих версиях, и к тому же потеря в производительности не очень большая, а в критичных местах можно переписать код на другой вариант. Вполне разумная цена за удобство использования.

    Делегаты


    Делегирование — это хорошая альтернатива наследованию, и Kotlin поддерживает его прямо из коробки. Рассмотрим простой пример с делегированием класса:

    //Kotlin
    package examples
     
    interface Base {
        fun print()
    }
     
    class BaseImpl(val x: Int) : Base {
        override fun print() { print(x) }
    }
     
    class Derived(b: Base) : Base by b {
        fun anotherMethod(): Unit {}
    }
    

    Класс Derived в конструкторе получает экземпляр класса, реализующий интерфейс Base, и в свою очередь делегирует реализацию всех методов интерфейса Base к передаваемому экземпляру. Декомпилированный код класса Derived будет выглядеть следующим образом:

    public final class Derived implements Base {
       private final Base $$delegate_0;
     
       public Derived(@NotNull Base b) {
          Intrinsics.checkParameterIsNotNull(b, "b");
          super();
          this.$$delegate_0 = b;
       }
     
       public void print() {
          this.$$delegate_0.print();
       }
    
       public final void anotherMethod() {
       }
    }
    

    В конструктор класса передается экземпляр класса, который запоминается в неизменяемом внутреннем поле. Также переопределяется метод print интерфейса Base, в котором просто происходит вызов метода из делегата. Все достаточно просто.

    Существует также возможность делегировать не только реализацию всего класса, но и отдельных его свойств (а с версии 1.1 еще возможно делегировать инициализацию в локальных переменных).

    Код на Kotlin:

    //Kotlin
    class DeleteExample {
        val name: String by Delegate()
    }
    

    Компилируется в код:

    public final class DeleteExample {
       @NotNull
       private final Delegate name$delegate = new Delegate();
     
      static final KProperty[] $$delegatedProperties = new KProperty[]{(KProperty)Reflection.property1(new PropertyReference1Impl(Reflection.getOrCreateKotlinClass(DeleteExample.class), "name", "getName()Ljava/lang/String;"))};
     
       @NotNull
       public final String getName() {
          return this.name$delegate.getValue(this, $$delegatedProperties[0]);
       }
    }
    

    При инициализации класса DeleteExample создается экземпляр класса Delegate, сохраняемый в поле name$delegate. И далее вызов функции getName переадресовывается к вызову функции getValue из name$delegate.

    В Kotlin есть уже несколько стандартных делегатов:

    — lazy, для ленивых вычислений значения поля.
    — observable, который позволяет получать уведомления обо всех изменения значения поля
    — map, используемый для инициализации значений поля из значений Map.

    Object и companion object


    В Kotlin нет модификатора static для методов и полей. Вместо них, по большей части, рекомендуется использовать функции на уровне файла. Если же нужно объявить функции, которые можно вызывать без экземпляра класса, то для этого есть object и companion object. Рассмотрим на примерах как они выглядят в байткоде:

    Простое объявление object с одним методом выглядит следующим образом:

    //Kotlin
    object ObjectExample {
        fun objectFun(): Int {
            return 1
        }
    }
    

    В коде дальше можно обращаться к методу objectFun без создания экземпляра ObjectExample. Код компилируется в практически каноничный синглтон:

    public final class ObjectExample {
       public static final ObjectExample INSTANCE;
     
       public final int objectFun() {
          return 1;
       }
     
       private ObjectExample() {
          INSTANCE = (ObjectExample)this;
       }
     
       static {
          new ObjectExample();
       }
    }
    

    И место вызова:

    //Kotlin
    val value = ObjectExample.objectFun()
    

    Компилируется к вызову INSTANCE:

    //Java
    int value = ObjectExample.INSTANCE.objectFun();
    

    companion object используется для создания аналогичных методов только уже в классе, для которого предполагается создание экземпляров.

    //Kotlin
    class ClassWithCompanion {
        val name: String = "Kurt"
        
        companion object {
            fun companionFun(): Int = 5
        }
    }
    
    //method call
    ClassWithCompanion.companionFun()
    

    Обращение к методу companionFun также не требует создания экземпляра класса, и в Kotlin будет выглядеть как простое обращение к статическому методу. Но на самом деле происходит обращение к компаньону класса. Посмотрим декомпилированный код:

    //Java
    public final class ClassWithCompanion {
       @NotNull
       private final String name = "Kurt";
       public static final ClassWithCompanion.Companion Companion = new ClassWithCompanion.Companion((DefaultConstructorMarker)null);
     
       @NotNull
       public final String getName() {
          return this.name;
       }
     
       public static final class Companion {
          public final int companionFun() {
             return 5;
          }
     
          private Companion() {
          }
     
          public Companion(DefaultConstructorMarker $constructor_marker) {
             this();
          }
       }
    }
    
    //вызов функции
    ClassWithCompanion.Companion.companionFun();
    

    Компилятор Kotlin упрощает вызовы, но из Java, правда, выглядит уже не так красиво. К счастью, есть возможность объявить методы по настоящему статическими. Для этого существует аннотация @JvmStatic. Ее можно добавить как к методам object, так и к методам companion object. Рассмотрим на примере object:

    //Kotlin
    object ObjectWithStatic {
        @JvmStatic
        fun staticFun(): Int {
            return 5
        }
    }
    

    В этом случае метод staticFun будет действительно объявлен статическим:

    public final class ObjectWithStatic {
       public static final ObjectWithStatic INSTANCE;
     
       @JvmStatic
       public static final int staticFun() {
          return 5;
       }
     
       private ObjectWithStatic() {
          INSTANCE = (ObjectWithStatic)this;
       }
     
       static {
          new ObjectWithStatic();
       }
    }
    

    Для методов из companion object тоже можно добавить аннотацию @JvmStatic:

    class ClassWithCompanionStatic {
        val name: String = "Kurt"
    
        companion object {
            @JvmStatic
            fun companionFun(): Int = 5
        }
    }
    

    Для такого кода будет также создан статичный метод companionFun. Но сам метод все равно будет вызывать метод из компаньона:

    public final class ClassWithCompanionStatic {
       @NotNull
       private final String name = "Kurt";
       public static final ClassWithCompanionStatic.Companion Companion = new ClassWithCompanionStatic.Companion((DefaultConstructorMarker)null);
    
       @NotNull
       public final String getName() {
          return this.name;
       }
    
       @JvmStatic
       public static final int companionFun() {
          return Companion.companionFun();
       }
    
       public static final class Companion {
          @JvmStatic
          public final int companionFun() {
             return 5;
          }
    
          private Companion() {
          }
    
          // $FF: synthetic method
          public Companion(DefaultConstructorMarker $constructor_marker) {
             this();
          }
       }
    }
    

    Как показано выше, Kotlin предоставляет различные возможности для объявления как статических методов так и методов компаньонов. Вызов статических методов чуть быстрее, поэтому в местах, где важна производительность, все же лучше ставить аннотации @JvmStatic на методы (но все равно не стоит рассчитывать на большой выигрыш в быстродействии)

    lateinit свойства


    Иногда возникает ситуация, когда нужно объявить notnull свойство в классе, значение для которого мы не можем сразу указать. Но при инициализации notnull поля мы обязаны присвоить ему значение по умолчанию, либо сделать свойство Nullable и записать в него null. Чтобы не переходить к nullable, в Kotlin существует специальный модификатор lateinit, который говорит компилятору Kotlin о том, что мы обязуемся сами позднее инициализировать свойство.

    //Kotlin
    class LateinitExample {
        lateinit var lateinitValue: String
    }
    

    Если же мы попробуем обратиться к свойству без инициализации, то будет брошено исключение UninitializedPropertyAccessException. Подобная функциональность работает достаточно просто:

    //Java
    public final class LateinitExample {
       @NotNull
       public String lateinitValue;
     
       @NotNull
       public final String getLateinitValue() {
          String var10000 = this.lateinitValue;
          if(this.lateinitValue == null) {
             Intrinsics.throwUninitializedPropertyAccessException("lateinitValue");
          }
     
          return var10000;
       }
     
       public final void setLateinitValue(@NotNull String var1) {
          Intrinsics.checkParameterIsNotNull(var1, "<set-?>");
          this.lateinitValue = var1;
       }
    }
    

    В getter вставляется дополнительная проверка значения свойства, и если в нем хранится null, то кидается исключение. Кстати именно из-за этого в Kotlin нельзя сделать lateinit свойство с типом Int, Long и других типов, которые соответствуют примитивным типам Java.

    coroutines


    В версии Kotlin 1.1 появилась новая функциональность, называемая корутины (coroutines). С ее помощью можно легко писать асинхронный код в синхронном виде. Помимо основной библиотеки (kotlinx-coroutines-core) для поддержки прерываний, есть еще и большой набор библиотек с различными расширениями:

    kotlinx-coroutines-jdk8 — дополнительная библиотека для JDK8
    kotlinx-coroutines-nio — расширения для асинхронного IO из JDK7+.

    kotlinx-coroutines-reactive — утилиты для реактивных стримов
    kotlinx-coroutines-reactor — утилиты для Reactor
    kotlinx-coroutines-rx1 — утилиты для RxJava 1.x
    kotlinx-coroutines-rx2 — утилиты для RxJava 2.x

    kotlinx-coroutines-android — UI контекст для Android.
    kotlinx-coroutines-javafx — JavaFx контекст для JavaFX UI приложений.
    kotlinx-coroutines-swing — Swing контекст для Swing UI приложений.

    Примечание: Функциональность пока находится в экспериментальной стадии, поэтому все сказанное ниже еще может измениться.

    Для того, чтобы обозначить, что функция может быть прервана и использована в контексте прерывания, используется модификатор suspend

    //Kotlin
    suspend fun asyncFun(x: Int): Int {
        return x * 3
    }
    

    Декомпилированный код выглядит следующим образом:

    //Java
    public static final Object asyncFun(int x, @NotNull Continuation $continuation) {
          Intrinsics.checkParameterIsNotNull($continuation, "$continuation");
          return Integer.valueOf(x * 3);
    }
    

    Получается практически исходная функция, за исключением того, что еще передается один дополнительный параметр, реализующий интерфейс Continuation.

    interface Continuation<in T> {
       val context: CoroutineContext
       fun resume(value: T)
       fun resumeWithException(exception: Throwable)
    }
    

    В нем хранится контекст выполнения, определена функция возвращения результата и функция возвращения исключения, в случае ошибки.

    Корутины компилируются в конечный автомат (state machine). Рассмотрим на примере:

    val a = a()
    val y = foo(a).await() // точка прерывания #1
    b()
    val z = bar(a, y).await() // точка прерывания #2
    c(z)
    

    Функции foo и bar возвращают CompletableFuture, на которых вызывается suspend функция await. Декомпилировать в Java такой код не получится (по большей части из-за goto), поэтому рассмотрим его в псевдокоде:

    class <anonymous_for_state_machine> extends CoroutineImpl<...> implements Continuation<Object> {
        // текущее состояние машины состояний
        int label = 0
        
        // локальные переменные корутин
        A a = null
        Y y = null
        
        void resume(Object data) {
            if (label == 0) goto L0
            if (label == 1) goto L1
            if (label == 2) goto L2
            else throw IllegalStateException()
            
          L0:
            a = a()
            label = 1
            data = foo(a).await(this) // 'this' передается как continuation 
            if (data == COROUTINE_SUSPENDED) return // возвращение, если await прервал выполнение
          L1:
            // внешний код возвращает выполнение корутины, передавая результат как data
            y = (Y) data
            b()
            label = 2
            data = bar(a, y).await(this) // 'this' передается как continuation
            if (data == COROUTINE_SUSPENDED) return // возвращение, если await прервал выполнение
          L2:
            // внешний код возвращает выполнение корутины передавая результат как data 
            Z z = (Z) data
            c(z)
            label = -1 // Не допускается больше никаких шагов
            return
        }          
    }    
    

    Как видно, получаются 3 состояния: L0, L1, L2. Выполнение начинается в состоянии L0, далее из которого происходит переключение в состояние L1 и после в L2. В конце происходит переключение состояния в -1 как индикация того, что больше никаких шагов не допускается.

    Сами корутины могут выполняться в различных потоках, есть удобный механизм для управления этим при помощи указания пула в контексте запуска корутины. Можно посмотреть подробный гайд с большим количеством примеров и описанием их использования.

    Все исходные коды на Kotlin доступны в github. Можно открыть их у себя и поэкспериментировать с кодом, параллельно просматривая, в какой итоговый байткод компилируются исходники.

    Выводы


    Производительность приложений на Kotlin будет не сильно хуже, чем на Java, а с использованием модификатора inline может даже оказаться лучше. Компилятор во всех местах старается генерировать наиболее оптимизированный байткод. Поэтому не стоит бояться, что при переходе на Kotlin вы получите большое ухудшение производительности. А в особо критичных местах, зная во что компилируется Kotlin, всегда можно переписать код на более подходящий вариант. Небольшая плата за то, что язык позволяет реализовывать сложные конструкции в достаточно лаконичном и простом виде.

    Спасибо за внимание! Надеюсь вам понравилась статья. Прошу всех тех, кто заметил какие-либо ошибки или неточности написать мне об этом в личном сообщении.
    • +10
    • 10,5k
    • 5

    ИНФОРИОН

    57,82

    Решения ИТ-инфраструктуры и защита информации

    Поделиться публикацией
    Комментарии 5
      +1
      Чтобы when с целочисленными константами оптимизировался (с использованием инструкции lookupswitch), их надо объявить именно как константы, то есть с модификатором const.
      const val ZERO = 0
      const val ONE = 1
        0
        Спасибо, добавил
        0

        Спасибо за статью!


        У вас, случайно, нет планов о написании аналогичного поста про Scala? Мне кажется, было бы любопытно сравнить реализацию тех фишек, которые для Kotlin и Scala общие.

          0
          Нет, планов писать подобный пост про Scala у меня нет =)

          Сама Scala уже давно появилась, если поискать то наверняка будет полно уже написанных постов. Как например этот пост.
            0

            Спасибо.

        Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

        Самое читаемое