Можно ли сделать java.lang.invoke.MethodHandle таким же быстрым, как прямой вызов?

Я сравниваю производительность MethodHandle::invoke и прямого вызова статических методов. Вот статический метод:

public class IntSum {
    public static int sum(int a, int b){
        return a + b;
    }
}

А вот и мой тест:

@State(Scope.Benchmark)
public class MyBenchmark {

    public int first;
    public int second;
    public final MethodHandle mhh;

    @Benchmark
    @OutputTimeUnit(TimeUnit.NANOSECONDS)
    @BenchmarkMode(Mode.AverageTime)
    public int directMethodCall() {
        return IntSum.sum(first, second);
    }

    @Benchmark
    @OutputTimeUnit(TimeUnit.NANOSECONDS)
    @BenchmarkMode(Mode.AverageTime)
    public int finalMethodHandle() throws Throwable {
        return (int) mhh.invoke(first, second);
    }

    public MyBenchmark() {
        MethodHandle mhhh = null;

        try {
            mhhh = MethodHandles.lookup().findStatic(IntSum.class, "sum", MethodType.methodType(int.class, int.class, int.class));
        } catch (NoSuchMethodException | IllegalAccessException e) {
            e.printStackTrace();
        }

        mhh = mhhh;
    }

    @Setup
    public void setup() throws Exception {
        first = 9857893;
        second = 893274;
    }
}

Получил следующий результат:

Benchmark                      Mode  Cnt  Score   Error  Units
MyBenchmark.directMethodCall   avgt    5  3.069 ± 0.077  ns/op
MyBenchmark.finalMethodHandle  avgt    5  6.234 ± 0.150  ns/op

MethodHandle имеет некоторое снижение производительности.

Запуск его с -prof perfasm показывает следующее:

....[Hottest Regions]...............................................................................
 31.21%   31.98%         C2, level 4  java.lang.invoke.LambdaForm$DMH::invokeStatic_II_I, version 490 (27 bytes) 
 26.57%   28.02%         C2, level 4  org.sample.generated.MyBenchmark_finalMethodHandle_jmhTest::finalMethodHandle_avgt_jmhStub, version 514 (84 bytes) 
 20.98%   28.15%         C2, level 4  org.openjdk.jmh.infra.Blackhole::consume, version 497 (44 bytes) 

Насколько я мог понять, причина результата теста в том, что Самый жаркий регион 2org.sample.generated.MyBenchmark_finalMethodHandle_jmhTest::finalMethodHandle_avgt_jmhStub содержит все проверки типов, выполняемые MethodHandle::invoke внутри цикла JHM. Фрагмент вывода сборки (часть кода опущена):

....[Hottest Region 2]..............................................................................
C2, level 4, org.sample.generated.MyBenchmark_finalMethodHandle_jmhTest::finalMethodHandle_avgt_jmhStub, version 519 (84 bytes) 
;...
0x00007fa2112119b0: mov     0x60(%rsp),%r10
;...
0x00007fa2112119d4: mov     0x14(%r12,%r11,8),%r8d  ;*getfield form
0x00007fa2112119d9: mov     0x1c(%r12,%r8,8),%r10d  ;*getfield customized
0x00007fa2112119de: test    %r10d,%r10d
0x00007fa2112119e1: je      0x7fa211211a65    ;*ifnonnull
0x00007fa2112119e7: lea     (%r12,%r11,8),%rsi
0x00007fa2112119eb: callq   0x7fa211046020    ;*invokevirtual invokeBasic
;...
0x00007fa211211a01: movzbl  0x94(%r10),%r10d  ;*getfield isDone
;...
0x00007fa211211a13: test    %r10d,%r10d
;jumping at the begging of jmh loop if not done
0x00007fa211211a16: je      0x7fa2112119b0    ;*aload_1 
;...

Перед вызовом invokeBasic мы выполняем проверку типа (внутри цикла jmh), которая влияет на вывод avgt.

ВОПРОС: Почему не вся проверка типов вынесена за пределы цикла? Я объявил public final MethodHandle mhh; внутри бенчмарка. Поэтому я ожидал, что компилятор сможет это понять и исключить те же проверки типов. Как убрать одинаковые проверки типов? Является ли это возможным?

Метод имеет сигнатуру MethodHandle.invoke(Object... args). Возможно ли, что значения int также автоматически упаковываются / распаковываются? Похоже, в этом классе много черной магии.

flakes 15.03.2018 06:56

@flakes Это сигнатурно-полиморфный метод, специально обработанный javac. Вы можете посмотреть скомпилированный байт-код. Подпись скомпилированного метода - MethodHandle.invoke(II)I

St.Antario 15.03.2018 06:58

Ах, это новая концепция для меня. Дикий!

flakes 15.03.2018 06:59

@flakes Кстати, @PolymorphicSignature не публичный. Мы не можем сами создать такие методы :).

St.Antario 15.03.2018 07:01

Но почему вы не используете invokeExact? А какой версией Java вы пользовались? При использовании Java 8 и наличии интерфейса с соответствующей подписью вы можете преобразовать дескрипторы прямых методов в реализации интерфейса через LambdaMetaFactory, как показано в этот ответ.

Holger 15.03.2018 12:39

@Holger Я тестировал invokeExact, но проблема заключалась в том, что я не добился улучшения производительности. Скомпилированный код тоже был таким же (проверки того же типа). В любом случае, invoke работает так же, как invokeExact, если MethodType совпадает, не так ли?

St.Antario 15.03.2018 12:48

Это зависит от версии JRE; Были реализации, в которых использование invoke было значительно медленнее, чем invokeExact, поэтому, если у вас есть выбор, отдайте предпочтение invokeExact. Если в вашей версии Java это не поможет, не повредит. Кстати, сколько у вас было итераций разминки? По моему опыту, ручки методов требуют большой разминки ...

Holger 15.03.2018 12:54

@Holger Я тестировал 5 разогрева и 5 итераций. Казалось, этого достаточно для выхода в устойчивое состояние ... Нет?

St.Antario 15.03.2018 13:37

@ St.Antario @PolymorphicSignature - компилятор перегружает ... :) конечно, мы не собираемся разбираться с ними. Кстати, @ForceInline также является частным, но у JMH каким-то образом есть @CompilerControl(CompilerControl.Mode.INLINE) (даже если указано, что это можно игнорировать)

Eugene 15.03.2018 16:02

@Eugene Я просто подумал, что может быть удобно иметь полиморфную подпись. Таким образом, я могу избежать ненужного преобразования бокса при возврате значения ...

St.Antario 16.03.2018 16:16

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

Holger 20.03.2018 09:34
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
Как вычислять биты и понимать побитовые операторы в Java - объяснение с примерами
Как вычислять биты и понимать побитовые операторы в Java - объяснение с примерами
В компьютерном программировании биты играют важнейшую роль в представлении и манипулировании данными на двоичном уровне. Побитовые операции...
Поднятие тревоги для долго выполняющихся методов в Spring Boot
Поднятие тревоги для долго выполняющихся методов в Spring Boot
Приходилось ли вам сталкиваться с требованиями, в которых вас могли попросить поднять тревогу или выдать ошибку, когда метод Java занимает больше...
Полный курс Java для разработчиков веб-сайтов и приложений
Полный курс Java для разработчиков веб-сайтов и приложений
Получите сертификат Java Web и Application Developer, используя наш курс.
13
11
1 211
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

Сделайте MethodHandle mhh статическим:

Benchmark            Mode  Samples  Score   Error  Units
directMethodCall     avgt        5  0,942 ± 0,095  ns/op
finalMethodHandle    avgt        5  0,906 ± 0,078  ns/op

Нестатический:

Benchmark            Mode  Samples  Score   Error  Units
directMethodCall     avgt        5  0,897 ± 0,059  ns/op
finalMethodHandle    avgt        5  4,041 ± 0,463  ns/op

Круто, действительно работает. Теперь MethodHandle::invoke и вызываемый им IntSum::sum просто встроены в цикл jmh. Почему? Что случилось? Возможно ли это в нестатическом случае?

St.Antario 15.03.2018 06:47

@ St.Antario Я согласен, почему добавление статики будет работать? : | Я думал, что это просто проблема с настройкой здесь, поэтому я кодирую свою собственную версию этого (с классом настройки), но результаты такие же, как и в вашем случае, разница в два раза больше ...

Eugene 15.03.2018 15:59
Ответ принят как подходящий

Вы используете вызов MethodHandleотражающий. Он работает примерно как Method.invoke, но с меньшим количеством проверок во время выполнения и без упаковки / распаковки. Поскольку этот MethodHandle не является static final, JVM не рассматривает его как константу, то есть цель MethodHandle является черным ящиком и не может быть встроена.

Несмотря на то, что mhh является окончательным, он содержит поля экземпляра, такие как MethodType type и LambdaForm form, которые перезагружаются на каждой итерации. Эти грузы не выводятся из цикла из-за внутреннего вызова черного ящика (см. Выше). Кроме того, LambdaForm в MethodHandle может быть изменен (настроен) во время выполнения между вызовами, так что потребности будет перезагружен.

Как сделать звонок быстрее?

  1. Используйте static final MethodHandle. JIT будет знать цель такого MethodHandle и, таким образом, может встроить его в сайт вызова.

  2. Даже если у вас нестатический MethodHandle, вы можете привязать его к статическому CallSite и вызывать его так же быстро, как прямые методы. Это похоже на то, как вызываются лямбды.

    private static final MutableCallSite callSite = new MutableCallSite(
            MethodType.methodType(int.class, int.class, int.class));
    private static final MethodHandle invoker = callSite.dynamicInvoker();
    
    public MethodHandle mh;
    
    public MyBenchmark() {
        mh = ...;
        callSite.setTarget(mh);
    }
    
    @Benchmark
    public int boundMethodHandle() throws Throwable {
        return (int) invoker.invokeExact(first, second);
    }
    
    1. Используйте обычный invokeinterface вместо MethodHandle.invoke, как предлагал @Holger. Экземпляр интерфейса для вызова данного MethodHandle может быть сгенерирован с помощью LambdaMetafactory.metafactory().

простите мое незнание, но если OP знает, что CallSite не изменится, можно ли использовать этот код вместо ConstantCallSite? Если это так, то, поскольку это постоянныйCallSite, потребует ли это, чтобы он также был статическим?

Eugene 15.03.2018 21:16

@Eugene ConstantCallSite требует указать целевой метод в конструкторе. В этом смысле ConstantCallSite бесполезен - это будет то же самое, что создать статический MethodHandle напрямую. MutableCallSite, с другой стороны, позволяет отложить решение о цели до более позднего времени во время выполнения.

apangin 15.03.2018 23:34

Ааа ... Значит, сворачивание констант применяется только к static final. По какой-то причине я подумал, что если мы объявим неизменяемое поле как final, компилятор может узнать, что оно является неизменным и окончательным, и поднять некоторую связанную проверку вне цикла (в моем случае). Может быть, вы знаете, где найти информацию о JIT-подъеме / постоянном складывании? Я посмотрел на пакет опто, но он кажется размытым ...

St.Antario 16.03.2018 11:58

Нестатические поля @ St.Antario Final по умолчанию не считаются константами, если не установлен -XX:+TrustFinalNonStaticFields. См. ciField::initialize_from.

apangin 16.03.2018 20:19

@Eugene Кстати, методы SignaturePolymorphic имеют строгое определение в JVMS. docs.oracle.com/javase/specs/jvms/se8/html/jvms-2.html#jvms-‌ 2.9 Итак, никаких других методов, кроме java.lang.invoke, быть не может.

St.Antario 17.03.2018 12:14

Я не понимаю вашего второго примера. Не могли бы вы заменить mh = ... конкретным заявлением? Это поможет сделать пример более понятным.

Gili 28.12.2018 04:30

@Gili mh указывает на вызываемый целевой метод. В исходном вопросе есть пример.

apangin 28.12.2018 09:03

@apangin В исходном вопросе упоминаются только mhh и mhhh, оба из которых являются MethodHandle. Надеюсь, вы понимаете, как это может сбивать с толку.

Gili 28.12.2018 15:40

Другие вопросы по теме