Подтвердить что ты не робот

Как я могу улучшить производительность Field.set(perhap using MethodHandles)?

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

Я хочу посмотреть, смогу ли я улучшить производительность, используя MethodHandle в Java 7. Итак, вот что у меня есть:

Вместо field.set(pojo, value) я делаю:

private static final Map<Field, MethodHandle> setHandles = new HashMap<>();

MethodHandle mh = setHandles.get(field);
if (mh == null) {
    mh = lookup.unreflectSetter(field);
    setHandles.put(field, mh);
}
mh.invoke(pojo, value);

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

Я читал, что использование invokeExact может быть быстрее, но когда я попытался использовать это, я получил java.lang.invoke.WrongMethodTypeException.

Кто-нибудь успешно смог оптимизировать повторные звонки в Field.set или Field.get?

4b9b3361

Ответ 1

2015-06-01: Обновлено, чтобы отразить комментарий @JoeC о другом случае, когда ручки статичны. Также обновлен до последней версии JMH и повторно запущен на современном оборудовании. Вывод остается практически таким же.

Пожалуйста, делайте правильный бенчмаркинг, возможно, это не так сложно с JMH. Как только вы это сделаете, ответ станет очевидным. Он также может продемонстрировать правильное использование invokeExact (требует компиляции и запуска target/source 1.7):

@Warmup(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS)
@Measurement(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS)
@Fork(3)
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
@State(Scope.Thread)
public class MHOpto {

    private int value = 42;

    private static final Field static_reflective;
    private static final MethodHandle static_unreflect;
    private static final MethodHandle static_mh;

    private static Field reflective;
    private static MethodHandle unreflect;
    private static MethodHandle mh;

    // We would normally use @Setup, but we need to initialize "static final" fields here...
    static {
        try {
            reflective = MHOpto.class.getDeclaredField("value");
            unreflect = MethodHandles.lookup().unreflectGetter(reflective);
            mh = MethodHandles.lookup().findGetter(MHOpto.class, "value", int.class);
            static_reflective = reflective;
            static_unreflect = unreflect;
            static_mh = mh;
        } catch (IllegalAccessException | NoSuchFieldException e) {
            throw new IllegalStateException(e);
        }
    }

    @Benchmark
    public int plain() {
        return value;
    }

    @Benchmark
    public int dynamic_reflect() throws InvocationTargetException, IllegalAccessException {
        return (int) reflective.get(this);
    }

    @Benchmark
    public int dynamic_unreflect_invoke() throws Throwable {
        return (int) unreflect.invoke(this);
    }

    @Benchmark
    public int dynamic_unreflect_invokeExact() throws Throwable {
        return (int) unreflect.invokeExact(this);
    }

    @Benchmark
    public int dynamic_mh_invoke() throws Throwable {
        return (int) mh.invoke(this);
    }

    @Benchmark
    public int dynamic_mh_invokeExact() throws Throwable {
        return (int) mh.invokeExact(this);
    }

    @Benchmark
    public int static_reflect() throws InvocationTargetException, IllegalAccessException {
        return (int) static_reflective.get(this);
    }

    @Benchmark
    public int static_unreflect_invoke() throws Throwable {
        return (int) static_unreflect.invoke(this);
    }

    @Benchmark
    public int static_unreflect_invokeExact() throws Throwable {
        return (int) static_unreflect.invokeExact(this);
    }

    @Benchmark
    public int static_mh_invoke() throws Throwable {
        return (int) static_mh.invoke(this);
    }

    @Benchmark
    public int static_mh_invokeExact() throws Throwable {
        return (int) static_mh.invokeExact(this);
    }

}

В 1x4x2 i7-4790K, JDK 8u40, Linux x86_64 он дает:

Benchmark                             Mode  Cnt  Score   Error  Units
MHOpto.dynamic_mh_invoke              avgt   25  4.393 ± 0.003  ns/op
MHOpto.dynamic_mh_invokeExact         avgt   25  4.394 ± 0.007  ns/op
MHOpto.dynamic_reflect                avgt   25  5.230 ± 0.020  ns/op
MHOpto.dynamic_unreflect_invoke       avgt   25  4.404 ± 0.023  ns/op
MHOpto.dynamic_unreflect_invokeExact  avgt   25  4.397 ± 0.014  ns/op
MHOpto.plain                          avgt   25  1.858 ± 0.002  ns/op
MHOpto.static_mh_invoke               avgt   25  1.862 ± 0.015  ns/op
MHOpto.static_mh_invokeExact          avgt   25  1.859 ± 0.002  ns/op
MHOpto.static_reflect                 avgt   25  4.274 ± 0.011  ns/op
MHOpto.static_unreflect_invoke        avgt   25  1.859 ± 0.002  ns/op
MHOpto.static_unreflect_invokeExact   avgt   25  1.858 ± 0.002  ns/op

..., который предполагает, что MH действительно намного быстрее, чем Reflection в этом конкретном случае (это потому, что проверка доступа против частного поля выполняется во время поиска, а не во время вызова). Примеры dynamic_* моделируют случай, когда MethodHandles и/или Fields не являются статически известными, например. вытащил из Map<String, MethodHandle> или что-то в этом роде. Наоборот, случаи static_* - это те, где invokers статически известны.

Обратите внимание, что отражающая производительность находится на одном уровне с MethodHandles в случаях dynamic_*, это связано с тем, что рефлексия в дальнейшем оптимизирована в JDK 8 (потому что на самом деле вам не нужна проверка доступа, чтобы читать ваши собственные поля), поэтому ответ может быть "просто" переключением на JDK 8;)

static_* случаи еще быстрее, потому что вызовы MethoHandles.invoke агрессивно встроены. Это устраняет часть проверки типов в случаях MH. Но в случаях рефлекса все еще есть быстрые проверки, и, следовательно, он отстает.

Ответ 2

Обновление: поскольку некоторые люди начали бессмысленное обсуждение "как тестировать", я хочу подчеркнуть решение вашей проблемы, содержащееся в моем ответе, прямо сейчас:

Вы можете использовать invokeExact даже в своем рефлексивном контексте, где у вас нет точной сигнатуры типа, преобразовывая MethodHandle с помощью asType в дескриптор Object в качестве аргументов. В средах, подверженных разнице в производительности между invoke и invokeExact, использование invokeExact в таком дескрипторе преобразования по-прежнему выполняется быстрее, чем использование invoke в дескрипторе прямого метода.


Оригинальный ответ:

Проблема в том, что вы не используете invokeExact. Ниже приведена небольшая контрольная программа, показывающая результаты различных способов увеличения поля int. Использование invoke вместо invokeExact приводит к снижению производительности ниже скорости отражения.

Вы получаете WrongMethodTypeException, потому что MethodHandle строго типизирован. Он ожидает точный тип соответствия подписи типа поля и владельца. Но вы можете использовать дескриптор для создания новой MethodHandle упаковки необходимых преобразований типов. Использование invokeExact в этом дескрипторе с использованием общей подписи (т.е. (Object,Object)Object) будет по-прежнему более эффективным, чем использование invoke с преобразованием динамического типа.

Результаты на моей машине с использованием 1.7.0_40:

direct        :   27,415ns
reflection    : 1088,462ns
method handle : 7133,221ns
mh invokeExact:   60,928ns
generic mh    :   68,025ns

и с использованием -server JVM получается недоумение

direct        :   26,953ns
reflection    :  629,161ns
method handle : 1513,226ns
mh invokeExact:   22,325ns
generic mh    :   43,608ns

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

И общий MethodHandle будет по-прежнему превосходить Reflection (пока используется invoke).

import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.reflect.Field;

public class FieldMethodHandle
{
  public static void main(String[] args)
  {
    final int warmup=1_000_000, iterations=1_000_000;
    for(int i=0; i<warmup; i++)
    {
      incDirect();
      incByReflection();
      incByDirectHandle();
      incByDirectHandleExact();
      incByGeneric();
    }
    long direct=0, refl=0, handle=0, invokeExact=0, genericH=0;
    for(int i=0; i<iterations; i++)
    {
      final long t0=System.nanoTime();
      incDirect();
      final long t1=System.nanoTime();
      incByReflection();
      final long t2=System.nanoTime();
      incByDirectHandle();
      final long t3=System.nanoTime();
      incByDirectHandleExact();
      final long t4=System.nanoTime();
      incByGeneric();
      final long t5=System.nanoTime();
      direct+=t1-t0;
      refl+=t2-t1;
      handle+=t3-t2;
      invokeExact+=t4-t3;
      genericH+=t5-t4;
    }
    final int result = VALUE.value;
    // check (use) the value to avoid over-optimizations
    if(result != (warmup+iterations)*5) throw new AssertionError();
    double r=1D/iterations;
    System.out.printf("%-14s:\t%8.3fns%n", "direct", direct*r);
    System.out.printf("%-14s:\t%8.3fns%n", "reflection", refl*r);
    System.out.printf("%-14s:\t%8.3fns%n", "method handle", handle*r);
    System.out.printf("%-14s:\t%8.3fns%n", "mh invokeExact", invokeExact*r);
    System.out.printf("%-14s:\t%8.3fns%n", "generic mh", genericH*r);
  }
  static class MyValueHolder
  {
    int value;
  }
  static final MyValueHolder VALUE=new MyValueHolder();

  static final MethodHandles.Lookup LOOKUP=MethodHandles.lookup();
  static final MethodHandle DIRECT_GET_MH, DIRECT_SET_MH;
  static final MethodHandle GENERIC_GET_MH, GENERIC_SET_MH;
  static final Field REFLECTION;
  static
  {
    try
    {
      REFLECTION = MyValueHolder.class.getDeclaredField("value");
      DIRECT_GET_MH = LOOKUP.unreflectGetter(REFLECTION);
      DIRECT_SET_MH = LOOKUP.unreflectSetter(REFLECTION);
      GENERIC_GET_MH = DIRECT_GET_MH.asType(DIRECT_GET_MH.type().generic());
      GENERIC_SET_MH = DIRECT_SET_MH.asType(DIRECT_SET_MH.type().generic());
    }
    catch(NoSuchFieldException | IllegalAccessException ex)
    {
      throw new ExceptionInInitializerError(ex);
    }
  }

  static void incDirect()
  {
    VALUE.value++;
  }
  static void incByReflection()
  {
    try
    {
      REFLECTION.setInt(VALUE, REFLECTION.getInt(VALUE)+1);
    }
    catch(IllegalAccessException ex)
    {
      throw new AssertionError(ex);
    }
  }
  static void incByDirectHandle()
  {
    try
    {
      Object target=VALUE;
      Object o=GENERIC_GET_MH.invoke(target);
      o=((Integer)o)+1;
      DIRECT_SET_MH.invoke(target, o);
    }
    catch(Throwable ex)
    {
      throw new AssertionError(ex);
    }
  }
  static void incByDirectHandleExact()
  {
    try
    {
      DIRECT_SET_MH.invokeExact(VALUE, (int)DIRECT_GET_MH.invokeExact(VALUE)+1);
    }
    catch(Throwable ex)
    {
      throw new AssertionError(ex);
    }
  }
  static void incByGeneric()
  {
    try
    {
      Object target=VALUE;
      Object o=GENERIC_GET_MH.invokeExact(target);
      o=((Integer)o)+1;
      o=GENERIC_SET_MH.invokeExact(target, o);
    }
    catch(Throwable ex)
    {
      throw new AssertionError(ex);
    }
  }
}

Ответ 3

Есть уловка 22 для MethodHandles в JDK 7 и 8 (я еще не тестировал JDK 9 или выше): MethodHandle работает быстро (так же быстро, как прямой доступ), если он находится в статическом поле. Иначе они такие медленные, как отражение. Если ваш фреймворк отражает более n получателей или установщиков, где n неизвестно во время компиляции, то MethodHandles, вероятно, для вас бесполезны.

Я написал статью, в которой сравнивали различные подходы к ускорению рефлексии.

Используйте LambdaMetafactory (или более экзотические подходы, такие как генерация кода), чтобы ускорить вызов методов получения и установки. Вот суть геттера (для сеттера используйте BiConsumer):

public final class MyAccessor {

    private final Function getterFunction;

    public MyAccessor() {
        MethodHandles.Lookup lookup = MethodHandles.lookup();
        CallSite site = LambdaMetafactory.metafactory(lookup,
                "apply",
                MethodType.methodType(Function.class),
                MethodType.methodType(Object.class, Object.class),
                lookup.findVirtual(Person.class, "getName", MethodType.methodType(String.class)),
                MethodType.methodType(String.class, Person.class));
        getterFunction = (Function) site.getTarget().invokeExact();
    }

    public Object executeGetter(Object bean) {
        return getterFunction.apply(bean);
    }

}

Ответ 4

ИЗОБРАЖЕНИЕ благодаря holger Я заметил, что я действительно должен был использовать invokeExact, поэтому я решил удалить материал из других jdks и использовать invokeExact только... используя -server или не все еще на самом деле для меня разница, хотя

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

Если вы посмотрите на это

class Test {
    public Object someField;
    public static void main(String[] args) throws Exception {
        Test t = new Test();
        Field field = Test.class.getDeclaredField("someField");
        Object value = new Object();
        for (int outer=0; outer<50; outer++) {
            long start = System.nanoTime();
            for (int i=0; i<100000000; i++) {
                field.set(t, value);
            }
            long time = (System.nanoTime()-start)/1000000;
            System.out.println("it took "+time+"ms");
        }
    }
}

Затем я нахожусь на своем компьютере раз 45000ms на jdk7u40 (jdk8 и pre 7u25 выполняют гораздо лучше, хотя)

Теперь посмотрим на ту же программу, используя дескрипторы

class Test {
    public Object someField;
    public static void main(String[] args) throws Throwable {
        Test t = new Test();
        Field field = Test.class.getDeclaredField("someField");
        MethodHandle mh = MethodHandles.lookup().unreflectSetter(field);
        Object value = new Object();
        for (int outer=0; outer<50; outer++) {
            long start = System.nanoTime();
            for (int i=0; i<100000000; i++) {
                mh.invokeExact(t, value);
            }
            long time = (System.nanoTime()-start)/1000000;
            System.out.println("it took "+time+"ms");
        }
    }
}

7u40 говорит примерно 1288 мс. Поэтому я могу подтвердить Холгер 30 раз на 7u40. На 7u06 этот код обрабатывается медленнее, потому что отражение было в несколько раз быстрее, а на jdk8 все новое снова.

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

Теперь самый большой проблемный вопрос... Я видел, что вы хотите это для Google appengine... И я должен сказать, что вы можете протестировать локально столько, сколько хотите, что в итоге означает, что производительность приложения на сайте Google будет. Afaik они используют модифицированный OpenJDK, но какая версия с какой модификацией они не говорят. Если Jdk7 окажется нестабильным, вам может быть не повезло или нет. Возможно, они добавили специальный код для размышлений, тогда все ставки все равно. И даже игнорируя это... возможно, модель оплаты снова изменилась, но обычно вы хотите избежать доступа к хранилищу данных путем кэширования, потому что это стоит. Если это все еще сохраняется, тогда реалистично, что любой дескриптор будет называться, скажем, в среднем 10 000 раз?