Почему Java 8 lambdas вызывается с помощью invokedynamic? - программирование
Подтвердить что ты не робот

Почему Java 8 lambdas вызывается с помощью invokedynamic?

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

Это полезно для динамических языков, где точный метод и типы аргументов неизвестны до выполнения. Но это не относится к Java lambdas. Они переводятся в статический метод с четко определенными аргументами. И этот метод можно вызвать с помощью invokestatic.

Итак, зачем нужна invokedynamic для lambdas, особенно когда есть удар производительности?

4b9b3361

Ответ 1

Lambdas не вызывается с помощью invokedynamic, их представление объекта создается с помощью invokedynamic, фактический вызов является регулярным invokevirtual или invokeinterface.

Например:

// creates an instance of (a subclass of) Consumer 
// with invokedynamic to java.lang.invoke.LambdaMetafactory 
something(x -> System.out.println(x));   

void something(Consumer<String> consumer) {
      // invokeinterface
      consumer.accept("hello"); 
}

Любая лямбда должна стать экземпляром некоторого базового класса или интерфейса. Этот экземпляр иногда содержит копию переменных, взятых из исходного метода, и иногда указатель на родительский объект. Это может быть реализовано как анонимный класс.

Почему invokedynamic

Короткий ответ: генерировать код во время выполнения.

Составители Java решили сгенерировать класс реализации во время выполнения. Это делается путем вызова java.lang.invoke.LambdaMetafactory.metafactory. Поскольку аргументы для этого вызова (тип возврата, интерфейс и захваченные параметры) могут измениться, для этого требуется invokedynamic.

Используя invokedynamic для создания анонимного класса во время выполнения, JVM позволяет генерировать этот байт-код класса во время выполнения. Последующие обращения к одному и тому же заявлению используют кешированную версию. Другой причиной использования invokedynamic является возможность изменения стратегии реализации в будущем без необходимости изменения уже скомпилированного кода.

Дорога не взята

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

something(new Consumer() { 
    public void accept(x) {
       // call to a generated method in the base class
       ImplementingClass.this.lambda$1(x);

       // or repeating the code (awful as it would require generating accesors):
       System.out.println(x);
    }
);   

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

О производительности

Первый вызов invokedynamic приведет к генерации анонимного класса. Затем код операции invokedynamic заменяется на код, что эквивалентно по производительности для написания вручную анонимного экземпляра.

Ответ 2

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

Стратегия перевода

Существует несколько способов представления лямбда-выражения в байт-код, такие как внутренние классы, обработчики методов, динамические прокси и другие. Каждый из этих подходов имеет свои плюсы и минусы. При выборе стратегии, существуют две конкурирующие цели: максимизация гибкости для будущей оптимизации, не совершая конкретной стратегии, против обеспечивая стабильность в представлении classfile. Мы можем добиться обе эти цели, используя функцию invokedynamic от JSR 292 до отделить двоичное представление создания лямбда в байтекоде от механики оценки лямбда-выражения во время выполнения. Вместо генерации байт-кода для создания объекта, который реализует выражение лямбда (например, вызов конструктора для внутреннего класс), мы описываем рецепт построения лямбда и делегируем фактическая конструкция для языка исполнения. Этот рецепт закодированные в статических и динамических списках аргументов invokedynamic инструкция.

Использование invokedynamic позволяет отложить выбор перевода стратегии до времени выполнения. Внедрение во время выполнения можно выбрать стратегии динамически оценивать лямбда-выражение. Время выполнения выбор реализации скрыт за стандартизированным (то есть частью спецификации платформы) API для конструкции лямбда, чтобы статический компилятор может вызывать вызовы для этого API и реализации JRE могут выбрать предпочтительную стратегию реализации. Вызываемая динамическая механики позволяют это делать без затрат на производительность, которые этот метод позднего связывания может иначе навязываться.

Когда компилятор встречает лямбда-выражение, он сначала понижает (desugars) лямбда-тела в метод, список аргументов и возвращаемый тип совпадает с типом лямбда-выражения, возможно с некоторыми дополнительные аргументы (для значений, взятых из лексической области, если любой). В тот момент, когда было бы захвачено лямбда-выражение, он генерирует вызываемый динамический сайт вызова, который при вызове возвращает экземпляр функционального интерфейса, к которому относится лямбда преобразованный. Этот сайт вызова называется lambda factory для данного лямбда. Динамическими аргументами для лямбда factory являются значения захваченных в лексической сфере. Метод бутстрапа лямбда factory - стандартизованный метод в библиотеке времени исполнения Java, называемый лямбда-метафактором. Учет статических бутстрапов информация, известная о лямбда во время компиляции (функциональная интерфейс, к которому он будет преобразован, дескриптор метода для информация о том, является ли тип SAM сериализуемый и т.д.)

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

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

Ответ 3

Текущая реализация лямбда Java 8 - сложное решение:

    • Скомпилировать лямбда-выражение для статического метода в классе-оболочке; вместо компиляции lambdas для разделения внутренних файлов классов (Scala компилируется таким образом, так что много файлов $$$ class вокруг)
    1. Ввести постоянный пул: BootstrapMethods, который переносит вызов статического метода на объект callsite (может быть кэширован для последующего использования)

Итак, чтобы ответить на ваш вопрос,

    1. Текущая реализация lambda с использованием invokedynamic немного быстрее, чем отдельный внутренний класс, потому что не нужно загружать эти внутренние файлы классов, а вместо этого создавать байт внутреннего класса [] на лету (чтобы удовлетворить, например, интерфейс Function ) и кэшируется для последующего использования.
  1. Команда JVM по-прежнему может генерировать отдельный внутренний класс (со ссылкой на статические методы класса), он гибкий