Я сравниваю производительность 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; внутри бенчмарка. Поэтому я ожидал, что компилятор сможет это понять и исключить те же проверки типов. Как убрать одинаковые проверки типов? Является ли это возможным?
@flakes Это сигнатурно-полиморфный метод, специально обработанный javac. Вы можете посмотреть скомпилированный байт-код. Подпись скомпилированного метода - MethodHandle.invoke(II)I
Ах, это новая концепция для меня. Дикий!
@flakes Кстати, @PolymorphicSignature не публичный. Мы не можем сами создать такие методы :).
Но почему вы не используете invokeExact? А какой версией Java вы пользовались? При использовании Java 8 и наличии интерфейса с соответствующей подписью вы можете преобразовать дескрипторы прямых методов в реализации интерфейса через LambdaMetaFactory, как показано в этот ответ.
@Holger Я тестировал invokeExact, но проблема заключалась в том, что я не добился улучшения производительности. Скомпилированный код тоже был таким же (проверки того же типа). В любом случае, invoke работает так же, как invokeExact, если MethodType совпадает, не так ли?
Это зависит от версии JRE; Были реализации, в которых использование invoke было значительно медленнее, чем invokeExact, поэтому, если у вас есть выбор, отдайте предпочтение invokeExact. Если в вашей версии Java это не поможет, не повредит. Кстати, сколько у вас было итераций разминки? По моему опыту, ручки методов требуют большой разминки ...
@Holger Я тестировал 5 разогрева и 5 итераций. Казалось, этого достаточно для выхода в устойчивое состояние ... Нет?
@ St.Antario @PolymorphicSignature - компилятор перегружает ... :) конечно, мы не собираемся разбираться с ними. Кстати, @ForceInline также является частным, но у JMH каким-то образом есть @CompilerControl(CompilerControl.Mode.INLINE) (даже если указано, что это можно игнорировать)
@Eugene Я просто подумал, что может быть удобно иметь полиморфную подпись. Таким образом, я могу избежать ненужного преобразования бокса при возврате значения ...
Я столкнулся с порогом порядка двадцати с дескрипторами методов, хотя он был с составными дескрипторами, и в случае нескольких преобразований каждый шаг, казалось, имел свой собственный счетчик, поэтому при работе с дескрипторами методов я всегда на всякий случай сделайте тест с действительно большим количеством итераций разминки. Другой вывод - по возможности использовать LambdaMetaFactory для прямых ручек.




Сделайте 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 Я согласен, почему добавление статики будет работать? : | Я думал, что это просто проблема с настройкой здесь, поэтому я кодирую свою собственную версию этого (с классом настройки), но результаты такие же, как и в вашем случае, разница в два раза больше ...
Вы используете вызов MethodHandleотражающий. Он работает примерно как Method.invoke, но с меньшим количеством проверок во время выполнения и без упаковки / распаковки. Поскольку этот MethodHandle не является static final, JVM не рассматривает его как константу, то есть цель MethodHandle является черным ящиком и не может быть встроена.
Несмотря на то, что mhh является окончательным, он содержит поля экземпляра, такие как MethodType type и LambdaForm form, которые перезагружаются на каждой итерации. Эти грузы не выводятся из цикла из-за внутреннего вызова черного ящика (см. Выше). Кроме того, LambdaForm в MethodHandle может быть изменен (настроен) во время выполнения между вызовами, так что потребности будет перезагружен.
Используйте static final MethodHandle. JIT будет знать цель такого MethodHandle и, таким образом, может встроить его в сайт вызова.
Даже если у вас нестатический 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);
}
простите мое незнание, но если OP знает, что CallSite не изменится, можно ли использовать этот код вместо ConstantCallSite? Если это так, то, поскольку это постоянныйCallSite, потребует ли это, чтобы он также был статическим?
@Eugene ConstantCallSite требует указать целевой метод в конструкторе. В этом смысле ConstantCallSite бесполезен - это будет то же самое, что создать статический MethodHandle напрямую. MutableCallSite, с другой стороны, позволяет отложить решение о цели до более позднего времени во время выполнения.
Ааа ... Значит, сворачивание констант применяется только к static final. По какой-то причине я подумал, что если мы объявим неизменяемое поле как final, компилятор может узнать, что оно является неизменным и окончательным, и поднять некоторую связанную проверку вне цикла (в моем случае). Может быть, вы знаете, где найти информацию о JIT-подъеме / постоянном складывании? Я посмотрел на пакет опто, но он кажется размытым ...
Нестатические поля @ St.Antario Final по умолчанию не считаются константами, если не установлен -XX:+TrustFinalNonStaticFields. См. ciField::initialize_from.
@Eugene Кстати, методы SignaturePolymorphic имеют строгое определение в JVMS. docs.oracle.com/javase/specs/jvms/se8/html/jvms-2.html#jvms- 2.9 Итак, никаких других методов, кроме java.lang.invoke, быть не может.
Я не понимаю вашего второго примера. Не могли бы вы заменить mh = ... конкретным заявлением? Это поможет сделать пример более понятным.
@Gili mh указывает на вызываемый целевой метод. В исходном вопросе есть пример.
@apangin В исходном вопросе упоминаются только mhh и mhhh, оба из которых являются MethodHandle. Надеюсь, вы понимаете, как это может сбивать с толку.
Метод имеет сигнатуру
MethodHandle.invoke(Object... args). Возможно ли, что значенияintтакже автоматически упаковываются / распаковываются? Похоже, в этом классе много черной магии.