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

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

Я хочу посмотреть, смогу ли я улучшить производительность, используя MethodHandles в Java7. Пока вот что у меня есть:

Вместо 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?

4 ответа

Решение

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_* случаи, когда вызывающие статически известны.

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

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

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

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


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

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

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

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

прямой:   27 415 нс
отражение: 1088 462 нс
дескриптор метода: 7133,221 нс
mh invokeExact:   60 928 нс
общий mh:   68,025 нс

и используя -server JVM уступает

прямой:   26 953 нс
отражение:  629,161нс
дескриптор метода: 1513,226 нс
mh invokeExact:   22,325 нс
общий mh:   43 608 нс

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

И общий MethodHandle будет по-прежнему превосходить отражение (при использовании 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);
    }
  }
}

РЕДАКТИРОВАТЬ благодаря 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 раз?

Есть уловка 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);
    }

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