Более быстрые альтернативы отражению Java [закрыто]


Как мы знаем, отражение является гибким, но медленно метод для поддержания и изменения поведения кода во время выполнения.

но если мы должны использовать такую функциональность, существуют ли более быстрые методы программирования в Java по сравнению с API отражения для динамических модификаций? Каковы плюсы и минусы этих альтернатив против рефлексии?

3   51   2013-10-24 09:40:05

3 ответа:

одной из альтернатив отражения является динамическое создание файла класса. Этот сгенерированный класс должен выполнить желаемое действие, например, вызывает метод, обнаруженный во время выполнения, и реализует interface известно во время компиляции, так что можно вызвать сгенерированный метод безотражательным способом, используя этот интерфейс. Есть одна загвоздка: если применимо, отражение делает тот же трюк внутренне. Это не работает в особых случаях, например, при вызове private метод, как вы не можете создайте файл юридического класса, вызывающий его. Таким образом, в реализации отражения существуют различные типы обработчиков вызовов, используя либо сгенерированный код, либо собственный код. Вы не можете победить это.

но более важным является то, что отражение выполняет проверку безопасности при каждом вызове. Таким образом, ваш сгенерированный класс будет проверен на загрузку и создание экземпляра только который может быть большим выигрышем. Но в качестве альтернативы вы можете использовать setAccessible(true) на Method экземпляр для включения проверки безопасности. Тогда только незначительная потеря производительности автобоксинга и создания массива varargs остается.

С Java 7 существует альтернатива обоим,MethodHandle. Большим преимуществом является то, что в отличие от двух других, он даже работает в ограниченных условиях безопасности. Проверка доступа для MethodHandle выполняются при его получении, но не при вызове его. Он имеет так называемую "полиморфную подпись", что означает, что вы можете вызывать его с произвольными типами аргументов без автоматического бокса или создание массивов. Конечно, неправильные типы аргументов создадут соответствующий RuntimeException.

(обновление) С Java 8, есть возможность использовать фоновую часть лямбда-выражения и функции языка ссылок на методы во время выполнения. Этот бэкэнд делает именно то, что описано в начале, генерируя класс динамически, который реализует interface ваш код может напрямую позвонить, когда он известен во время компиляции. Точная механика реализация специфична, следовательно, не определена, но вы можете предположить, что реализация попытается сделать вызов как можно быстрее. Текущая реализация JRE Oracle делает это отлично. Мало того, что это избавляет вас от бремени создания такого класса доступа, он также способен делать то, что вы никогда не могли сделать - вызвать даже private методы через сгенерированный код. Я обновил пример, чтобы включить это решение. В этом примере используется стандартный interface что уже существует и имеет желаемую сигнатуру метода. Если нет такого соответствия interface существует, вы должны создать свой собственный функциональный интерфейс доступа с помощью метода с правильной подписью. Но, конечно, теперь пример кода требует Java 8 для запуска.

вот простой пример теста:

import java.lang.invoke.LambdaMetafactory;
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodType;
import java.lang.reflect.Method;
import java.util.function.IntBinaryOperator;

public class TestMethodPerf
{
  private static final int ITERATIONS = 50_000_000;
  private static final int WARM_UP = 10;

  public static void main(String... args) throws Throwable
  {
 // hold result to prevent too much optimizations
    final int[] dummy=new int[4];

    Method reflected=TestMethodPerf.class
      .getDeclaredMethod("myMethod", int.class, int.class);
    final MethodHandles.Lookup lookup = MethodHandles.lookup();
    MethodHandle mh=lookup.unreflect(reflected);
    IntBinaryOperator lambda=(IntBinaryOperator)LambdaMetafactory.metafactory(
      lookup, "applyAsInt", MethodType.methodType(IntBinaryOperator.class),
      mh.type(), mh, mh.type()).getTarget().invokeExact();

    for(int i=0; i<WARM_UP; i++)
    {
      dummy[0]+=testDirect(dummy[0]);
      dummy[1]+=testLambda(dummy[1], lambda);
      dummy[2]+=testMH(dummy[1], mh);
      dummy[3]+=testReflection(dummy[2], reflected);
    }
    long t0=System.nanoTime();
    dummy[0]+=testDirect(dummy[0]);
    long t1=System.nanoTime();
    dummy[1]+=testLambda(dummy[1], lambda);
    long t2=System.nanoTime();
    dummy[2]+=testMH(dummy[1], mh);
    long t3=System.nanoTime();
    dummy[3]+=testReflection(dummy[2], reflected);
    long t4=System.nanoTime();
    System.out.printf("direct: %.2fs, lambda: %.2fs, mh: %.2fs, reflection: %.2fs%n",
      (t1-t0)*1e-9, (t2-t1)*1e-9, (t3-t2)*1e-9, (t4-t3)*1e-9);

    // do something with the results
    if(dummy[0]!=dummy[1] || dummy[0]!=dummy[2] || dummy[0]!=dummy[3])
      throw new AssertionError();
  }

  private static int testMH(int v, MethodHandle mh) throws Throwable
  {
    for(int i=0; i<ITERATIONS; i++)
      v+=(int)mh.invokeExact(1000, v);
    return v;
  }

  private static int testReflection(int v, Method mh) throws Throwable
  {
    for(int i=0; i<ITERATIONS; i++)
      v+=(int)mh.invoke(null, 1000, v);
    return v;
  }

  private static int testDirect(int v)
  {
    for(int i=0; i<ITERATIONS; i++)
      v+=myMethod(1000, v);
    return v;
  }

  private static int testLambda(int v, IntBinaryOperator accessor)
  {
    for(int i=0; i<ITERATIONS; i++)
      v+=accessor.applyAsInt(1000, v);
    return v;
  }

  private static int myMethod(int a, int b)
  {
    return a<b? a: b;
  }
}

старая программа, напечатанная в моей настройке Java 7:direct: 0,03s, mh: 0,32s, reflection: 1,05s, который предложил MethodHandle была хорошая альтернатива. Теперь обновленная программа работает под управлением Java 8 на той же машине напечатали direct: 0,02s, lambda: 0,02s, mh: 0,35s, reflection: 0,40s что ясно показывает, что производительность отражения была улучшена до такой степени, что может сделать дело с MethodHandle ненужно, если вы не используете его для выполнения лямбда-трюка, который явно превосходит все отражающие альтернативы, что неудивительно, поскольку это просто прямой вызов (ну, почти: один уровень косвенности). Обратите внимание, что я сделал целевой метод private чтобы продемонстрировать возможность вызова даже private методы эффективно.

как всегда, я должен указать на простоту этого эталона и насколько он искусственен. Но я думаю, что тенденция четко видна и, что еще более важно, результаты убедительно объяснимы.

Я создал небольшую библиотеку под названием лямда-завод. Он основан на LambdaMetafactory, но избавляет вас от хлопот поиска или создания интерфейса, который соответствует методу.

вот некоторые примеры времени выполнения для 10e8 итераций (воспроизводимых с помощью класса PerformanceTest):

лямбда: 0.02 s, прямая: 0.01 s, отражение: 4.64 s для метода (int, int)
Лямбда: 0,03 с, прямой: 0.02 с отражением: 3.23 S для способ(объект, int)

Допустим у нас есть класс с именем MyClass, который определяет следующие методы:

private static String myStaticMethod(int a, Integer b){ /*some logic*/ }
private float myInstanceMethod(String a, Boolean b){ /*some logic*/ }

мы можем получить доступ к этим методам, как это:

Method method = MyClass.class.getDeclaredMethod("myStaticMethod", int.class, Integer.class); //Regular reflection call
Lambda lambda = LambdaFactory.create(method);  
String result = (String) lambda.invoke_for_Object(1000, (Integer) 565); //Don't rely on auto boxing of arguments!

Method method = MyClass.class.getDeclaredMethod("myInstanceMethod", String.class, Boolean.class);
Lambda lambda = LambdaFactory.create(method);
float result = lambda.invoke_for_float(new MyClass(), "Hello", (Boolean) null);  //No need to cast primitive results!

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

в приведенном выше примере выбран invoke_for_float метод указывает, что мы вызываем метод, который возвращает a плыть. Если метод, к которому вы пытаетесь получить доступ, возвращает FX строку, коробочный примитив (Integer, Boolean и т. д.) или какой-либо пользовательский объект, вы должны вызвать invoke_for_Object.

проект является хорошим шаблоном для экспериментов с LambdaMetafactory, так как он содержит рабочий код для различных аспектов:

  1. статические вызовы и вызовы экземпляров
  2. доступ к закрытым методам и методам из других пакетов
  3. 'вызывает специальную' логику, т. е. где созданная реализация такова, что она обходит динамический метод dispatch.

альтернативой для отражения является использование интерфейса. Просто принимая от эффективная Java Джошуа Блоха.

мы можем получить многие из преимуществ отражения, неся при этом несколько из его стоимости можно использовать его только в очень ограниченном виде. Для многих программы, которые должны использовать класс, недоступный во время компиляции, во время компиляции существует соответствующий интерфейс или суперкласс который относится к классу. Если это так, вы можете создать экземпляры рефлексивно и доступ к ним обычно через их интерфейс или надкласс. Если конструктор не имеет параметров, то вы даже не нужно использовать Java.ленг.задумайтесь; класс.метод newInstance обеспечивает необходимую функциональность.

использовать отражение только для создания объекта, т. е.

// Reflective instantiation with interface access
   public static void main(String[] args) {
       // Translate the class name into a Class object
       Class<?> cl = null;
       try {
           cl = Class.forName(args[0]);
       } catch(ClassNotFoundException e) {
           System.err.println("Class not found.");
           System.exit(1);
       }
       // Instantiate the class
       Set<String> s = null;
       try {
           s = (Set<String>) cl.newInstance();
       } catch(IllegalAccessException e) {
           System.err.println("Class not accessible.");
           System.exit(1);
       } catch(InstantiationException e) {
           System.err.println("Class not instantiable.");
           System.exit(1);
       }
       // Exercise the set
       s.addAll(Arrays.asList(args).subList(1, args.length));
       System.out.println(s);
}

в то время как эта программа просто игрушка, техника она демонстрирует это очень мощный. Игрушечная программа может быть легко повернута в общий тестер набора, который проверяет реализацию указанного набора с помощью агрессивно манипулируя одним или несколькими экземплярами и проверяя, что они соблюдайте установленный контракт. Аналогичным образом, он может быть превращен в общий установите инструмент анализа производительности. На самом деле, техника достаточно мощный для реализации полномасштабной структуры поставщика услуг . Наиболее в свое время эта техника все, что вам нужно в пути отображение.

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