Nano Hash - криптовалюты, майнинг, программирование

Можно ли сделать 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) 

Насколько я мог понять, причина результатов теста заключается в том, что Самый горячий регион 2 org.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; внутри теста. Поэтому я ожидал, что компилятор сможет это понять и устранить те же проверки типов. Как устранить однотипные проверки типов? Является ли это возможным?

15.03.2018

  • У метода есть подписьMethodHandle.invoke(Object... args). Возможно ли, что значения int также автоматически упаковываются/распаковываются? Похоже, в этом классе много черной магии. 15.03.2018
  • @flakes Это сигнатурно-полиморфный метод, специально обработанный javac. Вы можете посмотреть на скомпилированный байт-код. Сигнатура скомпилированного метода MethodHandle.invoke(II)I 15.03.2018
  • О, это новая концепция для меня. Дикий! 15.03.2018
  • @flakes Кстати, @PolymorphicSignature не является общедоступным. Мы не можем сами создавать такие методы :). 15.03.2018
  • Но почему вы не используете invokeExact? И какую версию Java вы использовали? При использовании Java 8 и наличии интерфейса с соответствующей сигнатурой вы можете преобразовать дескрипторы прямых методов в реализации интерфейса через LambdaMetaFactory, как показано в этот ответ. 15.03.2018
  • @Holger Я проверил invokeExact, но проблема в том, что я не получил никакого улучшения производительности. Скомпилированный код тоже был одинаковым (однотипные проверки). В любом случае, invoke работает так же, как invokeExact, если совпадает MethodType, не так ли? 15.03.2018
  • Это зависит от версии JRE; были реализации, в которых invoke был значительно медленнее, чем invokeExact, поэтому, если у вас есть выбор, предпочтите invokeExact. Если это не поможет в вашей версии Java, это тоже не повредит. Кстати, сколько у вас было разминочных итераций? По моему опыту, дескрипторы методов нуждаются в длительном прогреве… 15.03.2018
  • @Holger Я проверил 5 разминок и 5 итераций. Казалось, этого достаточно для выхода на устойчивое состояние... Нет? 15.03.2018
  • @St.Antario @PolymorphicSignature - перегрузки компилятора... :) конечно, мы не должны их обрабатывать. Кстати, @ForceInline также является частным, но у JMH каким-то образом есть @CompilerControl(CompilerControl.Mode.INLINE) (даже если заявлено, что это можно игнорировать) 15.03.2018
  • @ Юджин Я просто подумал, что было бы удобно иметь полиморфную подпись. Таким образом, я могу избежать ненужного преобразования бокса при возврате значения... 16.03.2018
  • Я столкнулся с порогом порядка двадцати с дескрипторами методов, хотя это было с составными дескрипторами, и в случае нескольких преобразований каждый шаг, казалось, имел свой собственный счетчик, поэтому при работе с дескрипторами методов я всегда сделайте тест с действительно большим количеством итераций прогрева, просто чтобы быть уверенным. Другой вывод — использовать LambdaMetaFactory для прямых дескрипторов, когда это возможно. 20.03.2018

Ответы:


1

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

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

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

  1. Используйте static final дескриптор метода. 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. Use regular invokeinterface instead of MethodHandle.invoke as @Holger suggested. An instance of interface for calling given MethodHandle can be generated with LambdaMetafactory.metafactory().
15.03.2018
  • простите за мое невежество, но если OP знает, что CallSite не изменится, можно ли вместо этого использовать этот код для использования ConstantCallSite? Если да, то, поскольку это постоянная CallSite, требуется ли, чтобы она также была статической? 15.03.2018
  • @Eugene ConstantCallSite требует указать целевой метод в конструкторе. В этом смысле ConstantCallSite бесполезен - это будет то же самое, что создавать статический MethodHandle напрямую. MutableCallSite, с другой стороны, позволяет отложить решение о цели до более позднего времени выполнения. 16.03.2018
  • Аааа... Это означает, что сворачивание констант применяется только к static final. Я почему-то подумал, что если мы объявим неизменяемое поле как final, компилятор сможет узнать, что оно неизменяемое и окончательное, и поднимет некоторую связанную проверку вне цикла (в моем случае). Может быть, вы знаете, где можно найти информацию о подъеме/складывании констант JIT? Я просмотрел opto пакет, но он кажется размытым... 16.03.2018
  • @St.Antario Final Нестатические поля не считаются константами по умолчанию, если не установлено -XX:+TrustFinalNonStaticFields. См. ciField::initialize_from . 16.03.2018
  • @Eugene Кстати, методы SignaturePolymorphic имеют строгое определение в JVMS. docs.oracle.com/javase/ specs/jvms/se8/html/jvms-2.html#jvms-2.9 Таким образом, никаких других методов, кроме тех, что в java.lang.invoke, быть не может. 17.03.2018
  • Я не понимаю вашего второго примера. Не могли бы вы заменить mh = ... конкретным утверждением? Это помогло бы сделать пример более понятным. 28.12.2018
  • @Gili mh указывает на вызываемый целевой метод. В исходном вопросе есть пример. 28.12.2018
  • @apangin В исходном вопросе упоминаются только mhh и mhhh, оба из которых являются MethodHandle. Надеюсь, вы понимаете, как это может запутать. 28.12.2018

  • 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
    
    15.03.2018
  • Круто, действительно работает. Теперь MethodHandle::invoke и фактический IntSum::sum, который он вызывает, просто встроены в цикл jmh. Почему? Что случилось? Возможно ли это сделать в нестатическом случае? 15.03.2018
  • @St.Antario St.Antario Я согласен, почему добавление статики сработает? :| Я думал, что это просто проблема с настройкой здесь, поэтому я кодирую свою собственную версию этого (с классом установки), но результаты такие же, как и в вашем случае, разница в два раза... 15.03.2018
  • Новые материалы

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

    Как написать эффективное резюме
    Предложения по дизайну и макету, чтобы представить себя профессионально Вам не позвонили на собеседование после того, как вы несколько раз подали заявку на работу своей мечты? У вас может..

    Частный метод Python: улучшение инкапсуляции и безопасности
    Введение Python — универсальный и мощный язык программирования, известный своей простотой и удобством использования. Одной из ключевых особенностей, отличающих Python от других языков, является..

    Как я автоматизирую тестирование с помощью Jest
    Шутка для победы, когда дело касается автоматизации тестирования Одной очень важной частью разработки программного обеспечения является автоматизация тестирования, поскольку она создает..

    Работа с векторными символическими архитектурами, часть 4 (искусственный интеллект)
    Hyperseed: неконтролируемое обучение с векторными символическими архитектурами (arXiv) Автор: Евгений Осипов , Сачин Кахавала , Диланта Хапутантри , Тимал Кемпития , Дасвин Де Сильва ,..

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

    Обеспечение масштабируемости LLM: облачный анализ с помощью AWS Fargate и Copilot
    В динамичной области искусственного интеллекта все большее распространение получают модели больших языков (LLM). Они жизненно важны для различных приложений, таких как интеллектуальные..