Привет! Меня зовут Григорий Юрков, и я уже несколько лет работаю в инфраструктурной команде Яндекс Маркета. Два года назад мы начали разрабатывать свой легковесный DI-фреймворк Scout, который предоставляет выразительный Kotlin DSL. Он не генерирует код, а делает всю работу в рантайме.

Недавний переход с compile-time-библиотеки Dagger 2 на нашу привёл к замедлению старта приложения. Подробнее об этом и о том пути, который мы прошли от идеи до публикации в опенсорс, можно прочитать в статье моего коллеги Александра Миронычева.

В этой статье мы будем подробно рассматривать то, как применение байт-кода помогло сохранить скорость на том же уровне и спасти проект по миграции на Scout.

Весь код в этой статье упрощён для понимания. Также не стоит забывать, что библиотека постоянно изменяется и улучшается. Если вы хотите посмотреть настоящий код, который представлен в библиотеке, то прошу в наш GitHub.

Почему стало медленнее

Чтобы вникнуть в проблему, которая возникла перед нами, предлагаю немного познакомиться с библиотекой. Вот так выглядит самый простой пример кода на Scout:

class MyClass
	
val scope = scope("my-scope") {
    factory<String> { "my-string" }
    factory<MyClass> { MyClass() }
}
	
fun main() {
    println(scope.get<String>()) // "my-string"
    println(scope.get<MyClass>()) // MyClass
}

Мы создаём factory для двух типов — String и MyClass. Эти factory лежат в общем Scope под названием my-scope. С помощью get()мы можем получить указанные типы, используя зарегистрированные в Scope фабрики.

Простейшая реализация Scope выглядела бы так:

typealias Key = Class<*>
	
object Keys {
    @JvmStatic fun create(keyClass: Class<*>): Key = keyClass
}

class Scope {
    internal val factories = HashMap<Key, () -> Any?>()
		
    inline fun <reified T> factory(noinline factory: () -> T) {
        val key: Key = Keys.create(T::class.java)
        factories[key] = factory
    }
        
    inline fun <reified T> get() {
        val key: Key = Keys.create(T::class.java)
        return scope.factories[key].invoke()
    }
}

Этот код сильно упрощён, но смысл остаётся тот же: мы просто берем HashMap и по классу складываем туда наши factory, а потом по тому же классу достаём и аллоцируем объект. 

Такой код работает сильно медленнее Dagger 2, и тому есть две основные причины:

  1. Мы используем Class в качестве ключа.

  2. Вызов T::class.java триггерит class loading.

С первым вроде всё понятно: HashMap постоянно вызывает hashCode() и equals(), чтобы класть и доставать объекты. Для Class эти функции не такие быстрые, как хотелось бы.

Со вторым всё интереснее. Когда мы вызываем MyClass::class.java, чтобы получить Class, мы триггерим загрузку этого класса в оперативную память. Это означает, что Java Virtual Machine (или Android Runtime) нужно прочитать класс из JAR- или DEX-файла и запарсить его. Этот процесс очень долгий, и мы бы хотели его избежать как минимум на этапе создания factory.

Как сделать быстрее

Мы можем убить двух птиц одним камнем, заменив ключи нашей мапы на Int. Тогда мы избавимся от HashMap в пользу чего-то более быстрого, а также избавимся от class loading. Осталось самое сложное: как заменить Class на Int?

Первое, что пришло в голову, — написать плагин на компилятор Kotlin, чтобы он заменял Class на Int. Но, во-первых, было совершенно непонятно, как подходить к этой проблеме. Во-вторых, мы опасались, что будем сильно зависеть от версии Kotlin и его внутреннего API, а это непременно доставит нам хлопот в будущем.

Тогда мне пришла в голову идея: а что, если заменить
typealias Key = Class<> на typealias Key = Int и кидать ошибку в функции create

Попробуем изменить код: 

typealias Key = Int // Замена с Class<*> на Int
	
object Keys {
    @JvmStatic fun create(keyClass: Class<*>): Key {
        throw NotImplementedError() // Кидаем ошибку
    }
}
    
// Scope остался неизменным
class Scope {
    internal val factories = HashMap<Key, () -> Any?>()

    inline fun <reified T> factory(noinline factory: () -> T) {
        val key: Key = Keys.create(T::class.java)
        factories[key] = factory
    }
        
    inline fun <reified T> get() {
        val key: Key = Keys.create(T::class.java)
        return scope.factories[key].invoke()
    }
}

Вы спросите: «А зачем кидать ошибку? Как тогда это должно работать?»

Логика проста: мы напишем процессор, который после компиляции кода в байт-код будет заменять вызовы Key.create на константы, и исключение не будет кидаться вообще. А если уж оно и произойдёт, то это будет служить маркером того, что процессор отработал неверно.

Теперь посмотрим на байт-код нашего примера, преобразованный в Java-код для наглядности:

class MainKt {
    public static final Scope scope = new Scope();
	
    static {
        int key;
	
        // Это байт-код для factory<String> 
        key = Keys.create(String.class); // Сейчас этот метод кинет исключение, если всё оставить как есть
        scope.factories.put(key, new Lambda1());
		
        // А это байт-код для factory<MyClass>
        key = Keys.create(MyClass.class);
        scope.factories.put(key, new Lambda2()); 
    }
    
    public static void main(String[] args) {
        int key;
        
        // Это вывод строки
        key = Keys.create(String.class);
        System.out.println(scope.factories.get(key).invoke());
        
        // А это вывод MyClass
        key = Keys.create(MyClass.class);
        System.out.println(scope.factories.get(key).invoke());
    }
}

Теперь, когда процессор будет проходиться по байт-коду, он будет встречать вызовы типа Keys.create(SomeClass.class) и заменять их на константы. Условимся, что String — это 1, а MyClass — это 2. В итоге получаем следующий изменённый байт-код:

class MainKt {
    public static final Scope scope = new Scope();
	
    static {
        int key;
	
        // Это байт-код для factory<String> 
        key = 1;
        scope.factories.put(key, new Lambda1());
		
        // А это байт-код для factory<MyClass>
        key = 2;
        scope.factories.put(key, new Lambda2()); 
    }
    
    public static void main(String[] args) {
        int key;
        
        // Это вывод строки
        key = 1;
        System.out.println(scope.factories.get(key).invoke());
        
        // А это вывод MyClass
        key = 2;
        System.out.println(scope.factories.get(key).invoke());
    }
}

Вызовов метода create нигде не осталось, так как мы всё заменили на константы. Исключения не кидаются, и всё работает!

А дальше я расскажу, как написать такой процессор и правильно его применять.

Как устроен байт-код

Байт-код — стандартное промежуточное представление, в которое компьютерная программа может быть переведена автоматическими средствами. Так говорит «Википедия». В случае JVM он хранится в .class-файлах, которые чаще всего можно найти упакованными в JAR-файлы (по сути, это обычный ZIP-архив).

Прежде чем перейдём к устройству байт-кода, расскажу о том, что такое обратная польская запись (ОПЗ). Дело в том, что байт-код работает по такому же принципу. Если вы знаете, что это такое, то можете пропустить эту часть.

Обратная польская запись

Допустим, у нас есть простое математическое выражение:

(1 + 2) * (3 + 4)

Минус такой записи в том, что у разных операторов — разные приоритеты (поэтому и нужны скобки). Также разные операторы могут быть разных типов: префиксными, бинарными, постфиксными. Ещё отдельно от операторов существуют функции. Логично, что в таком виде компьютеру будет сложно вычислять значение выражения. Поэтому представим его в виде ОПЗ:

1 2 + 3 4 + *

Все операторы и функции пишутся после операндов, и теперь у них одинаковый приоритет (поэтому скобки не нужны).

Исполнить такое выражение можно с помощью обычной стековой машины. Алгоритм следующий:

Идём по математическому выражению с начала.

	Если встречаем константу, 

       Tо кладем её в стек.

	Иначе, если встречаем оператор или функцию (в ОПЗ это, в принципе, одни и те же вещи), 

       То достаём из стека нужное количество операндов, выполняем операцию и кладём в стек получившееся значение.

В конце в стеке остаётся наш результат.

Вернёмся к байт-коду.

JVM — это навороченная стековая машина, которая читает подряд инструкции, а потом кладёт в стек или достаёт из него значения. Приведу пример:

System.out.println(1 + 2 * 3);

Напишем байт-код этого Java-кода (он может не полностью соответствовать тому, что генерирует Java). После каждой инструкции я буду выводить стек, чтобы было понятно, как исполняется байт-код.

// Стек: пустой
GETFIELD java.lang.System.out // Загружаем значение из статического поля в стек
// Стек: PrintStream
LDC 2 // Загружаем константы в стек
LDC 3
// Стек: PrintStream, 2, 3
IMULT // Выполняем умножение и кладём результат в стек
// Стек: PrintStream, 6
LDC 1 // Загружаем единичку
// Стек: PrintStream, 6, 1
IADD // Выполняем сложение
// Стек: PrintStream, 7
INVOKEVIRTUAL println // Вызываем нестатический метод println на объекте PrintStream с аргументом 7
// Стек остался пустым, а в консоли напечаталось число 7

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

  • LDC — загрузка константы в стек.

  • IMULT, IADD — операции сложения и умножения. Буква I в начале означает операцию над целыми числами. Для double есть аналогичные с буквой D.

  • INVOKEVIRTUAL — вызов нестатического метода. Для вызова статического используется INVOKESTATIC.

  • GETFIELD — получение нестатического поля. Для статического есть GETSTATIC.

Также хочется отметить такие немаловажные инструкции, как LOAD и STORE. Первая загружает переменную в стек, а вторая выгружает из стека в переменную.

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

Например, INVOKESTATIC 42 означает, что в пуле констант под номером 42 лежит ссылка на метод. Так JVM определяет, какой метод нужно вызвать. Для простоты обычно сразу пишут название метода, чтобы не нужно было лезть в пул констант.

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

Этих базовых знаний будет достаточно, чтобы написать наш процессор.

Как пропатчить байт-код

После того как мы разобрались с устройством байт-кода, нужно понять, какие инструкции нам надо заменить. Итак, у нас есть следующий код:

Keys.create(SomeClass.class);

Если посмотреть на байт-код, то тут Java или Kotlin генерируют всего две инструкции:

LDC SomeClass
INVOKESTATIC Keys.create

В LDC передаётся ссылка на класс, поэтому она загружает в стек наш Class-объект, а INVOKESTATIC просто вызывает метод create. Вместо этого мы хотим загрузить обычную Int-константу. Для этого подойдёт SIPUSH-инструкция:

SIPUSH 42

Это такая же инструкция, как и LDC, которая загружает константу в стек, но она не использует ссылку на пул констант, а сразу же после себя содержит значение. Минус в том, что её аргумент ограничен 2 байтами, поэтому значения не могут быть больше 2^15 (32 768), но нам пока этого хватит.

Во всей этой магии по замене инструкций нам сильно поможет библиотека BCEL (Byte Code Engineering Library). Она предоставляет очень удобный API, поэтому я выбрал именно её. Сначала напишем метод, который пробежится по всем методам класса и достанет оттуда связанный список инструкций:

fun modifyClass(file: File) {
    // Парсим .class-файл
    val parser = ClassParser(file.path)
    val javaClass = parser.parse()
    val classGen = ClassGen(javaClass)

    // Получаем пул констант
    val constantPoolGen = classGen.constantPool

    for (method in classGen.methods) {
        val methodGen = MethodGen(method, classGen.className, constantPoolGen)

        modifyMethod(methodGen.instructionList, constantPoolGen)

        // Сохраняем изменения в методе
        classGen.replaceMethod(method, methodGen.method)
    }

    // Сохраняем изменения в классе
    classGen.javaClass.dump(FileOutputStream(file))
}

Теперь реализуем modifyMethod(), который будет заменять инструкции:

fun modifyMethod(
    list: InstructionList,
    constantPoolGen: ConstantPoolGen
) {
    var instruction: InstructionHandle? = null

    while (true) {
        // Перебираем все инструкции — в этой библиотеке они представлены связанным списком
        instruction = (if (instruction == null) list.start else instruction.next) ?: break

        // Первая инструкция — это LDC
        val ldc = instruction.instruction as? LDC ?: continue
        // Убеждаемся, что LDC-инструкция загружает константу класса
        val objectType = ldc.getValue(constantPoolGen) as? ObjectType ?: continue
        // Узнаём имя этого класса
        val className = objectType.className ?: continue

        // Следующей инструкцией должна быть INVOKESTATIC
        val invokestatic = instruction.next?.instruction as? INVOKESTATIC ?: continue
        // Убеждаемся, что мы вызываем метод Keys.create(Class<*>): Int
        if (invokestatic.getLoadClassType(constantPoolGen).className != "Keys") continue
        if (invokestatic.getName(constantPoolGen) != "create") continue
        if (invokestatic.getSignature(constantPoolGen) != "(Ljava/lang/Class)I") continue
        // Сигнатура (Ljava/lang/Class)I означает, что метод принимает один аргумент с типом Class, а возвращает Int


        // Определяем индекс для нашего класса: просто берём следующий свободный
        var index = indexMap[className]
        if (index == null) {
            index = indexMap.size
            indexMap[className] = index // Сохраняем индекс под текущее имя класса
        }

        // Удаляем инструкцию LDC, заменяем её на инструкцию NOP, которая ничего не делает
        instruction.instruction = InstructionConst.NOP
        // Заменяем INVOKESTATIC-инструкцию на SIPUSH, которая будет пушить в стек наш индекс
        instruction.next.instruction = SIPUSH(index.toShort())
    }
}

Наш процессор готов! 

Однако теперь возникает вопрос: откуда нам взять .class-файлы для их изменения? С JVM всё просто: JAR-файл содержит нужные .class-файлы. А что касается Android, то внутри APK у нас лежат .dex-файлы. Это тоже байт-код, но не Java, а Dalvik. Он компилируется из байт-кода Java, и в этом случае нам нужно вставить процессор перед этим шагом.

Как интегрировать процессор в систему сборки

Поскольку мы, как и многие разработчики, используем систему сборки Gradle, внедрим наш процессор именно в неё.

Чтобы начать работу с Gradle, достаточно понять: основная единица работы в нём — это Task. Таски могут быть связаны: например, задача компиляции байт-кода Dalvik зависит от задачи компиляции Kotlin в байт-код Java. Все задачи в Gradle объединены в проекты, но мы их называем просто модулями.

Акцентируем внимание на таске compileKotlin. Он обрабатывает наши .kt-файлы (исходники Kotlin), создавая .class-файлы (байт-код JVM), а это именно то, что нам нужно.

В корневом файле build.gradle.kts добавим следующий код:

allprojects {
    // Проходимся по всем таскам всех проектов. 
    tasks.configureEach {
        // Отфильтровываем все таски с названием compileKotlin
        if (name == "compileKotlin") {
            // doLast вызывает лямбду сразу после того,
            // как таск compileKotlin закончит своё выполнение
            doLast {
                // Проходимся по всем выходным папкам таска compileKotlin
                // Там и должны лежать наши .class-файлы
                outputs.files.forEach { output ->
                    output.walk()
                        .filter { file -> file.isFile && file.extension == "class" } // Фильтруем все .class-файлы 
                        .forEach { file ->
                            // Изменяем наши классы методом, который мы описали ранее
                            // Этот метод должен лежать где-то в модуле buildSrc, чтобы он был виден во всех Gradle-скриптах
                            modifyClass(file)
                        }
                }
            }
        }
    }
}

Теперь наш процессор будет запускаться всегда, когда мы компилируем код Kotlin. Неважно, для чего нам это понадобится — для компиляции Android или JVM-приложения, — он всегда будет запускаться и модифицировать байт-код.

Результаты

Вы спросите, как наши изменения повлияли на скорость?

Это скриншот перформанс-тестов Android-приложения Яндекс Маркета. После переноса всего проекта на Int-ключи мы вернулись к прежним значениям времени старта приложения. На скриншоте также видно предыдущую попытку ускорения, о которой можно прочитать в первой статье про Scout.

Работа с байт-кодом оказалась увлекательным занятием. Он достаточно прост для внесения изменений и не так страшен, как чистый ассемблер. JVM проверяет байт-код и в случае ошибки в генерации выдаёт исключение VerifyError с детальным описанием возникшей проблемы.

Я надеюсь, что этот материал поможет вам впоследствии рассматривать свои задачи через призму байт-кода, который, как видно на нашем примере, может оказаться весьма полезным инструментом.

Комментарии (36)


  1. arTk_ev
    01.11.2023 07:25

    Зачем использовать фреймворк DI, который жрет перф и требует правки байт-кода?

    Он выполняет примитивную функцию, которую может заменить ServiceLess на несколько строк кода или фабрики.


    1. acelost
      01.11.2023 07:25

      Фреймворк не требует правки байт-кода, но правка байт-кода его ускоряет – это важная деталь. Про ServiceLess никогда не слышал, даже нагуглить не получается с ходу. Что это за зверь?


      1. arTk_ev
        01.11.2023 07:25

        https://sergeyteplyakov.blogspot.com/2013/03/di-service-locator.html

        Имел в виду ServiceLocator. Примитивная реализация DI, никаких расходов.

        Просто ни разу в практике не приходилось встречаться с задачами, где бы понадобилась сложная реализация DI со сложными зависимостями.


        1. rpuxaa1 Автор
          01.11.2023 07:25

          Так Scout и является ServiceLocator. Сервисы надо хранить в Map и доставать их по какому-то ключу. Это и замедлило наше приложение


          1. arTk_ev
            01.11.2023 07:25

            Теперь понятно, спасибо.


        1. develmax
          01.11.2023 07:25
          +2

          ServiceLocator это антипаттерн, усложняет unit-тестирование и добавляет лишнюю зависимость на конкретную реализацию DI SL


          1. rpuxaa1 Автор
            01.11.2023 07:25

            А как он усложняет Unit тестирование, если в Unit тестировании нету DI


            1. develmax
              01.11.2023 07:25
              +1

              То что нужно перед запуском теста подготовить этот SL, в зависимости от того как написан код, SL может создавать проблемы или делать невозможным параллельное выполнение тестов.


  1. a_artikov
    01.11.2023 07:25
    +1

    Привет. Крутая статья.

    А как модификация байткода влияет на время сборки проекта?


    1. rpuxaa1 Автор
      01.11.2023 07:25

      В маркете я замерял - получалось секунд 10 на монолитный модуль. А вообще процессор опциональный, его можно выключать для debug сборок.


  1. alexejisma
    01.11.2023 07:25

    Работает ли scout корректно с generic классами?


    1. rpuxaa1 Автор
      01.11.2023 07:25

      Нет, так как ключ основан на java.lang.Class в котором нет информации о generic. Можно использовать котлиновский KType, который сохраняет не только дженерики но и даже nullable был тип или нет. Мы когда-то пробовали использовать его, но получали ухудшения по сравнению с обычным классом. С приходом байткод процессора, KType можно так же успешно заменять на int, но в планах такого у нас пока нет.


      1. alexejisma
        01.11.2023 07:25

        Понял. Спасибо!


  1. slonopotamus
    01.11.2023 07:25

    Когда мы вызываем MyClass::class.java, чтобы получить Class, мы триггерим загрузку этого класса в оперативную память.

    Непонятно. Зачем регистрировать а DI классы, которые вы не будете загружать? Или вы хотите избежать их загрузки на старте, а вместо этого получать фризы в рандомные моменты работы приложения?


    1. rpuxaa1 Автор
      01.11.2023 07:25

      Чтобы получить даже один объект из графа нужно его полностью проинициализировать, иначе непонятно, если ли внутри графа фабрика нужного типа или нет. Dagger это делает при компиляции, а manual DI (в том числе и Scout) делают это в рантайме.

      В момент инициализации мы складываем в Map пары Class - Factory. А так как ключ у нас является классом, то он как раз и триггерит class loading


      1. slonopotamus
        01.11.2023 07:25

        Ну вот я и спрашиваю. Вы ж в процессе работы приложения рано или поздно из графа все объекты повытаскиваете. И рано или поздно потратите время на загрузку классов. Но либо это происходит на старте, когда юзер готов к тому что "падажжите, мы грузимся", либо это будет происходить посреди работы приложения и делать фризы.


        1. olku
          01.11.2023 07:25

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


        1. rpuxaa1 Автор
          01.11.2023 07:25

          Нет, далеко не все повытаскиваем.
          И лучше как раз сделать быстрый старт, а потом лениво грузить классы - так юзер не заметит лагов


          1. slonopotamus
            01.11.2023 07:25

            Нет, далеко не все повытаскиваем.

            Зачем тогда их вообще было класть туда?

            так юзер не заметит лагов

            Непонятно. Придут волшебные гномики и сделают загрузку классов мгновенной?


            1. rpuxaa1 Автор
              01.11.2023 07:25

              Ну так юзер может использовать не все фичи приложения, например. Поэтому и не понадобятся все зависимости.

              Непонятно. Придут волшебные гномики и сделают загрузку классов мгновенной?

              Потому что мы размазываем инициализацию на все время пользования приложением. Условно, много просадок по 20мс не ощутится так же как одна просадка в одну секунду.


              1. slonopotamus
                01.11.2023 07:25

                Условно, много просадок по 20мс не ощутится так же как одна просадка в одну секунду.

                Ну это смотря что за приложение. Я не думаю что вам бы понравилось играть в игру, смотреть видео или слушать музыку с рандомными просадками по 20мс.


  1. Lucker216
    01.11.2023 07:25

    Хорошая статья, но главный вопрос по scout остался открытым

    Когда полная поддержка KMP? А то koin кажется единственный, кто работает и с kotlin multipltform и даже с compose multiplatform


    1. acelost
      01.11.2023 07:25
      +1

      Обязательно сообщим, когда поддержим kmp. Сейчас это приоритетное направление развития


      1. Lucker216
        01.11.2023 07:25

        Благодарю за ответ, а то уже думал начать ковырять kotlin-inject и пытаться его завести на мультиплатформе, а то koin имеет проблему с верификацией
        К слову в статье это не указано, но коин тоже имеет возможность тестирования в unit-test


        1. acelost
          01.11.2023 07:25

          Для верификации Scout достаточно написать один unit-тест и забыть о верификации на долгие месяцы. Koin же требует указывать в тесте каждый новый модуль (либо я не знаю о какой-то важной части этой библиотеки)


  1. klee0kai
    01.11.2023 07:25

    Пару вопросов:
    Как поддерживается в такой схеме подключения кэширование тасок в Gradle?
    Как я понял, у вас реализация на основе BCEL? сравнивали ли с AspectJ ? И как я понимаю в любом случае планируете переписать на KSP ?
    И как я понимаю, чтоб добиться бОльшей оптимизации можно было бы использовать SparseArray ?


    1. rpuxaa1 Автор
      01.11.2023 07:25

      Как поддерживается в такой схеме подключения кэширование тасок в Gradle?

      Процессор выполняется не в отдельной таске, а с помощью doLast на таске компиляции котлина, поэтому кэширование работает само по себе без лишних танцев с бубном (хотя я пытался сначала сделать отдельной таской и ничего не получилось)

      Как я понял, у вас реализация на основе BCEL? 

      Все верно

      Сравнивали ли с AspectJ?

      Нет. Даже не задумывался

      И как я понимаю в любом случае планируете переписать на KSP?

      Нет, а как он нам тут может помочь? Я лично жду выхода Kotlin 2.0, где обещали стабильный API для плагинов на компилятор. Возможно, перепишу потом процессор через него - так мы уйдем от зависимости на систему сборки.

      И как я понимаю, чтоб добиться бОльшей оптимизации можно было бы использовать SparseArray?

      SparceArray это оптимизация по памяти, но не по скорости, так как он использует бинарный поиск по капотом. Были попытки написать свою мапу, но ее скорость очень сильно зависела от версии JDK и не было очевидно, быстрее она или нет. В итоге пока что оставили HashMap, может потом придумаем, на что можно заменить.


  1. klee0kai
    01.11.2023 07:25

    И еще вопрос, получается у вас нельзя расширять скоупы от модуля к модулю в многомодульном проекте gradle? так как константы Int просто так не передашь между подпроектами в монорепе?


    1. rpuxaa1 Автор
      01.11.2023 07:25

      Можно. Главное подключить скрипт в корневой build.gradle, о чем в я статье и говорю


  1. aamonster
    01.11.2023 07:25

    Это используется в приложении, поставляемом пользователям?


    1. rpuxaa1 Автор
      01.11.2023 07:25

      Да, в Яндекс Маркете под андроид.


  1. amberovsky
    01.11.2023 07:25

    Мы для таких целей (не-джава) используем прокси класс-обёртку/функцию или статический метод. У вас так нельзя?


    1. rpuxaa1 Автор
      01.11.2023 07:25

      Для каких целей? Не джава это что конкретно?


  1. ivan2kh
    01.11.2023 07:25

    Планетарка зачетная


  1. Maccimo
    01.11.2023 07:25

    Для Class эти функции не такие быстрые, как хотелось бы.

    У Class и equals() и hashCode() наследуются от java.lang.Object.
    Куда уж быстрее-то?

    Прежде чем перейдём к устройству байт-кода, расскажу о том, что такое обратная польская запись (ОПЗ).

    Вспоминается анекдот № 301205. Шучу, если что.

    System.out.println(1 + 2 * 3);

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

    И не инструкцией ldc, а инструкцией bipush.

    IMULT

    «T» здесь лишнее.

    Для этого подойдёт SIPUSH-инструкция: ... Минус в том, что её аргумент ограничен 2 байтами, поэтому значения не могут быть больше 2^15 (32 768)

    Я  слышал, что на собеседованиях в Яндекс есть алгоритмическая секция ;]

    For short, from -32768 to 32767, inclusive
    JLS 17 § 4.2.1

     

    Во всей этой магии по замене инструкций нам сильно поможет библиотека BCEL (Byte Code Engineering Library).

    BCEL в 2023 году?
    Закопайте стюардессу и возьмите хотя бы ASM или модный-молодёжный Byte Buddy.

    Она предоставляет очень удобный API

    Очень удобный???!!!
    Да вы троллите!


    1. rpuxaa1 Автор
      01.11.2023 07:25

      Я  слышал, что на собеседованиях в Яндекс есть алгоритмическая секция ;]

      И причем тут алгоритмическая секция?

      возьмите хотя бы ASM или модный-молодёжный Byte Buddy.

      Я пытался сначала сделать через них, но я не нашел удобных инструментов для замены байткода.

      Очень удобный???!!!

      Я так написал, потому что для текущей задачи, мне нужно было получить массив инструкций (или связанный список), что BCEL мне и предоставил, а предыдущие библиотеки - нет.