IT TIP

Java의 반영에 대한 더 빠른 대안

itqueen 2021. 1. 5. 20:45
반응형

Java의 반영에 대한 더 빠른 대안


아시다시피 리플렉션은 런타임에 코드의 동작을 유지하고 수정 하는 유연하지만 느린 방법입니다.

그러나 이러한 기능을 사용해야한다면 동적 수정을 위해 Reflection API에 비해 Java에서 더 빠른 프로그래밍 기술이 있습니까? 반성에 대한 이러한 대안의 장단점은 무엇입니까?


Reflection에 대한 한 가지 대안은 클래스 파일을 동적으로 생성하는 것입니다. 이 생성 된 클래스는 원하는 작업을 수행해야합니다. 예를 들어 런타임에 발견 된 메서드를 호출하고 interface컴파일 타임에 알려진 것을 구현 하여 해당 인터페이스를 사용하여 생성 된 메서드를 비 반사 방식으로 호출 할 수 있습니다. 한 가지 문제가 있습니다. 적용 가능한 경우 Reflection은 내부적으로 동일한 트릭을 수행합니다. 이것은 특별한 경우에 작동하지 않습니다. 예를 들어 private메소드를 호출 하는 합법적 인 클래스 파일을 생성 할 수 없기 때문에 메소드를 호출 할 때입니다. 따라서 Reflection 구현에는 생성 된 코드 또는 네이티브 코드를 사용하는 다양한 유형의 호출 핸들러가 있습니다. 당신은 그것을 이길 수 없습니다.

그러나 더 중요한 것은 Reflection이 모든 호출에 대해 보안 검사를 수행한다는 것입니다. 따라서 생성 된 클래스는로드 및 인스턴스화시에만 확인되며 이는 큰 승리가 될 수 있습니다. 그러나 다른 방법은 호출 할 수 있습니다 setAccessible(true)A의 Method의 보안 검사를 설정하는 예. 그러면 autoboxing 및 varargs 배열 생성의 사소한 성능 손실 만 남습니다.

Java 7 부터는 둘 다에 대한 대안이 MethodHandle있습니다. 큰 장점은 다른 두 가지와 달리 보안이 제한된 환경에서도 작동한다는 것입니다. a에 대한 액세스 검사는 MethodHandle획득시 수행되지만 호출시에는 수행되지 않습니다. 그것은 소위 "다형 서명"을 가지고 있습니다. 즉, 자동 박싱이나 배열 생성없이 임의의 인수 유형으로 호출 할 수 있습니다. 물론 잘못된 인수 유형은 적절한 RuntimeException.

( 업데이트 ) Java 8 에는 런타임에 람다 표현식 및 메서드 참조 언어 기능의 백엔드를 사용하는 옵션이 있습니다. 이 백엔드는 처음에 설명한 작업을 정확히 수행하여 interface컴파일 타임에 코드를 직접 호출 할 수 있는 코드 를 구현하는 클래스를 동적으로 생성합니다 . 정확한 메커니즘은 구현에 따라 다르므로 정의되지 않았지만 구현이 가능한 한 빨리 호출을 수행하는 것이 최선이라고 가정 할 수 있습니다. 현재 Oracle JRE 구현은이를 완벽하게 수행합니다. 이렇게하면 그러한 접근 자 클래스를 생성해야하는 부담에서 벗어날뿐만 아니라 결코 할 수없는 작업을 수행 할 수 있습니다.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;
  }
}

Th old program printed in my Java 7 setup: direct: 0,03s, mh: 0,32s, reflection: 1,05s which suggested that MethodHandle was a good alternative. Now, the updated program running under Java 8 on the same machine printed direct: 0,02s, lambda: 0,02s, mh: 0,35s, reflection: 0,40s which clearly shows that Reflection performance has been improved to a degree that might make dealing with MethodHandle unnecessary, unless you use it to do the lambda trick, that clearly outperforms all reflective alternatives, which comes at no surprise, as it is just a direct call (well, almost: one level of indirection). Note that I made the target method private to demonstrate the capability of calling even private methods efficiently.

As always, I have to point at the simplicity of this benchmark and how artificial it is. But I think, the tendency is clearly visible and even more important, the results are convincingly explainable.


I have created a small library called lambda-factory. It is based on LambdaMetafactory, but saves you the hassle of finding or creating an interface that matches the method.

Here are some sample runtimes for 10E8 iterations (reproducable with the class PerformanceTest):

Lambda: 0.02s, Direct: 0.01s, Reflection: 4.64s for method(int, int)
Lambda: 0.03s, Direct: 0.02s, Reflection: 3.23s for method(Object, int)

Let's say we have a class called MyClass, which defines the following methods:

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

We can access these methods like this:

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!

Notice that when invoking the lambda, you must choose an invocation method that contains the target method's return type in its name. - varargs and auto boxing were too expensive.

In the example above, the chosen invoke_for_float method indicates that we are invoking a method, which returns a float. If the method you are trying to access returns fx a String, a boxed primitive (Integer, Boolean etc) or some custom Object, you would call invoke_for_Object.

The project is a good template for experimenting with LambdaMetafactory since it contains working code for various aspects:

  1. static calls and instance calls
  2. Access to private methods, and methods from other packages
  3. 'invokeSpecial' logic, i.e. where the created implementation is such, that it bypasses dynamic method dispatch.

The alternate for reflection is using Interface. Just taking from Effective Java by Joshua Bloch.

We can obtain many of the benefits of reflection while incurring few of its costs by using it only in a very limited form. For many programs that must use a class that is unavailable at compile time, there exists at compile time an appropriate interface or superclass by which to refer to the class. If this is the case, you can create instances reflectively and access them normally via their interface or superclass. If the appropriate constructor has no parameters, then you don’t even need to use java.lang.reflect; the Class.newInstance method provides the required functionality.

Use reflection for only for creating the object i.e.

// 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);
}

While this program is just a toy, the technique it demonstrates is very powerful. The toy program could easily be turned into a generic set tester that validates the specified Set implementation by aggressively manipulating one or more instances and checking that they obey the Set contract. Similarly, it could be turned into a generic set performance analysis tool. In fact, the technique is sufficiently powerful to implement a full-blown service provider framework . Most of the time, this technique is all that you need in the way of reflection.

This example demonstrates two disadvantages of reflection. First, the example can generate three runtime errors, all of which would have been compile-time errors if reflective instantiation were not used. Second, it takes twenty lines of tedious code to generate an instance of the class from its name, whereas a con- structor invocation would fit neatly on a single line. These disadvantages are, however, restricted to the part of the program that instantiates the object. Once instantiated, it is indistinguishable from any other Set instance.

ReferenceURL : https://stackoverflow.com/questions/19557829/faster-alternatives-to-javas-reflection

반응형