programing

Java 리플렉션 퍼포먼스

prostudy 2022. 6. 30. 22:49
반응형

Java 리플렉션 퍼포먼스

클래스 생성자를 호출하는 대신 리플렉션(reflection)을 사용하여 개체를 만드는 것이 성능의 큰 차이를 초래합니까?

네, 물론입니다.반사를 통해 클래스를 조회하는 것은 규모 면에서 더 비쌉니다.

성찰에 관한 Java 문서 인용:

리플렉션에는 동적으로 확인되는 유형이 포함되므로 특정 Java 가상 시스템 최적화를 수행할 수 없습니다.따라서 반사 연산은 비반사 연산에 비해 성능이 느리므로 성능에 민감한 애플리케이션에서 자주 호출되는 코드 섹션에서는 피해야 합니다.

다음은 Sun JRE 6u10을 실행하는 기계로 5분 만에 해킹한 간단한 테스트입니다.

public class Main {

    public static void main(String[] args) throws Exception
    {
        doRegular();
        doReflection();
    }

    public static void doRegular() throws Exception
    {
        long start = System.currentTimeMillis();
        for (int i=0; i<1000000; i++)
        {
            A a = new A();
            a.doSomeThing();
        }
        System.out.println(System.currentTimeMillis() - start);
    }

    public static void doReflection() throws Exception
    {
        long start = System.currentTimeMillis();
        for (int i=0; i<1000000; i++)
        {
            A a = (A) Class.forName("misc.A").newInstance();
            a.doSomeThing();
        }
        System.out.println(System.currentTimeMillis() - start);
    }
}

다음과 같은 결과를 얻을 수 있습니다.

35 // no reflection
465 // using reflection

룩업과 인스턴스화는 함께 수행되며 경우에 따라 룩업을 리팩터링할 수 있지만 이는 기본적인 예에 불과합니다.

인스턴스화만 해도 퍼포먼스에 적중합니다.

30 // no reflection
47 // reflection using one lookup, only instantiating

다시 YMMV.

네, 더 느려요.

하지만 제1의 법칙을 기억하세요.최신 최적화는 모든 악의 근원입니다.

(DRY의 경우 #1과 동점이 될 수 있음)

맹세컨데, 만약 누군가가 나에게 와서 이런 질문을 한다면, 나는 앞으로 몇 달 동안 그들의 코드를 매우 주의깊게 볼 것이다.

필요할 때까지 최적화해서는 안 됩니다.그때까지, 읽기 쉬운 좋은 코드를 써 주세요.

아, 그리고 바보같은 코드를 쓰라는 것도 아니에요.가능한 한 깔끔한 방법을 생각해 보세요.복사 및 붙여넣기 등은 불필요합니다(내부 루프와 같은 것을 주의해 주세요.필요에 가장 적합한 컬렉션을 사용하세요.이러한 것을 「최적화되지 않은」프로그래밍은 아니고, 「나쁜」프로그래밍입니다).

이런 질문을 들으면 겁이 나지만, 모든 사람이 스스로 모든 규칙을 익혀야만 제대로 이해할 수 있다는 것을 잊는다.「최적화」한 유저의 디버깅을 수개월에 걸쳐 실시하면, 취득할 수 있습니다.

편집:

이 실타래에서 재미있는 일이 일어났다.#1의 답을 확인해 주세요.컴파일러의 최적화 능력을 보여주는 예입니다.이 테스트는 비반사 인스턴스화를 완전히 배제할 수 있기 때문에 완전히 무효입니다.

교훈? 깔끔하고 깔끔하게 코드화된 솔루션을 작성하여 너무 느리다는 것을 증명하기 전에는 절대 최적화하지 마십시오.

A a = new A()가 JVM에 의해 최적화되고 있는 것을 발견할 수 있습니다.오브젝트를 배열에 배치하면 퍼포먼스가 저하됩니다.;) 다음 인쇄는...

new A(), 141 ns
A.class.newInstance(), 266 ns
new A(), 103 ns
A.class.newInstance(), 261 ns

public class Run {
    private static final int RUNS = 3000000;

    public static class A {
    }

    public static void main(String[] args) throws Exception {
        doRegular();
        doReflection();
        doRegular();
        doReflection();
    }

    public static void doRegular() throws Exception {
        A[] as = new A[RUNS];
        long start = System.nanoTime();
        for (int i = 0; i < RUNS; i++) {
            as[i] = new A();
        }
        System.out.printf("new A(), %,d ns%n", (System.nanoTime() - start)/RUNS);
    }

    public static void doReflection() throws Exception {
        A[] as = new A[RUNS];
        long start = System.nanoTime();
        for (int i = 0; i < RUNS; i++) {
            as[i] = A.class.newInstance();
        }
        System.out.printf("A.class.newInstance(), %,d ns%n", (System.nanoTime() - start)/RUNS);
    }
}

이것은 내 기계에서 약 150ns의 차이를 시사합니다.

리플렉션보다 더 빠른 무언가가 필요하고, 단순히 너무 이른 최적화가 아닌 경우에는 ASM 또는 더 높은 수준의 라이브러리를 사용한 바이트 코드 생성을 선택할 수 있습니다.바이트 코드를 처음 생성하는 것은 리플렉션을 사용하는 것보다 느리지만 바이트 코드가 생성되면 일반 Java 코드만큼 빠르며 JIT 컴파일러에 의해 최적화됩니다.

코드 생성을 사용하는 어플리케이션의 예를 다음에 나타냅니다.

  • CGLIB에 의해 생성된 프록시에서 메서드를 호출하는 것은 Java의 다이내믹 프록시보다 약간 빠릅니다.이는 CGLIB가 프록시의 바이트 코드를 생성하기 때문입니다.다이나믹 프록시는 리플렉션만을 사용하기 때문입니다(메서드 호출에서는 CGLIB가 약 10배 빠른 것을 측정했지만 프록시 작성은 느렸습니다).

  • J Serial은 반사를 사용하는 대신 직렬화된 개체의 필드를 읽고 쓰기 위한 바이트 코드를 생성합니다.J Serial 사이트에는 몇 가지 벤치마크가 있습니다.

  • 100% 확신할 수는 없지만(소스를 읽고 싶지는 않지만), Guice는 의존성 주입을 위해 바이트 코드를 생성한다고 생각합니다.내가 틀렸다면 고쳐 주세요.

"Significant"는 전적으로 컨텍스트에 의존합니다.

리플렉션을 사용하여 일부 구성 파일을 기반으로 단일 핸들러 개체를 만들고 나머지 시간을 데이터베이스 쿼리를 실행하는 데 사용할 경우 이 작업은 중요하지 않습니다.좁은 루프에서 반사를 통해 많은 개체를 생성할 경우, 이는 매우 중요합니다.

일반적으로 설계상의 유연성(필요한 경우!)은 퍼포먼스가 아닌 반사를 사용하는 원동력이 됩니다.그러나 성능이 문제인지 여부를 판단하려면 토론 포럼에서 임의 응답을 얻는 대신 프로파일을 작성해야 합니다.

리플렉션으로 인한 오버헤드는 다소 있지만, 최신 VM에서는 이전보다 훨씬 줄어듭니다.

프로그램의 모든 간단한 개체를 만들기 위해 반사를 사용한다면 뭔가 잘못된 것입니다.그럴 만한 이유가 있을 때 가끔 사용하는 것은 전혀 문제가 되지 않을 것입니다.

예, Reflection을 사용하면 성능이 저하되지만 최적화를 위한 가능한 해결 방법은 다음과 같은 방법을 캐싱하는 것입니다.

  Method md = null;     // Call while looking up the method at each iteration.
      millis = System.currentTimeMillis( );
      for (idx = 0; idx < CALL_AMOUNT; idx++) {
        md = ri.getClass( ).getMethod("getValue", null);
        md.invoke(ri, null);
      }

      System.out.println("Calling method " + CALL_AMOUNT+ " times reflexively with lookup took " + (System.currentTimeMillis( ) - millis) + " millis");



      // Call using a cache of the method.

      md = ri.getClass( ).getMethod("getValue", null);
      millis = System.currentTimeMillis( );
      for (idx = 0; idx < CALL_AMOUNT; idx++) {
        md.invoke(ri, null);
      }
      System.out.println("Calling method " + CALL_AMOUNT + " times reflexively with cache took " + (System.currentTimeMillis( ) - millis) + " millis");

결과는 다음과 같습니다.

[java] 조회 시 반사적으로 1000000회 호출 메서드 5618밀리 소요

[java] 캐시를 사용한 반사적으로 1000000회 호출 방식에는 270밀리초가 소요되었습니다.

흥미롭게도 보안 검사를 건너뛰는 set Accessible(true)을 설정하면 비용이 20% 절감됩니다.

set Accessible 없음(true)

new A(), 70 ns
A.class.newInstance(), 214 ns
new A(), 84 ns
A.class.newInstance(), 229 ns

set Accessible(true) 포함

new A(), 69 ns
A.class.newInstance(), 159 ns
new A(), 85 ns
A.class.newInstance(), 171 ns

오브젝트 할당이 반사의 다른 측면만큼 희망적이지 않지만 반사는 느립니다.반사 기반 인스턴스화로 동등한 성능을 얻으려면 코드를 작성해야 합니다. 그래야 JIT가 인스턴스화된 클래스를 알 수 있습니다.클래스의 ID를 확인할 수 없는 경우 할당 코드를 인라인으로 지정할 수 없습니다.더욱이 이스케이프 분석이 실패하고 개체를 스택에 할당할 수 없습니다.운이 좋으면 이 코드가 핫 상태가 되면 JVM의 런타임 프로파일링을 통해 문제가 해결될 수 있으며, 어떤 클래스가 우세한지 동적으로 판단하여 이 클래스에 최적화될 수 있습니다.

이 실의 마이크로 벤치마크에 심각한 결함이 있다는 것을 알아두시기 바랍니다. 그러니 잘 생각해 보세요.가장 큰 결함이 없는 것은 피터 로레이의 것이다.그것은 방법을 짜맞추기 위해 워밍업을 하고, 실제로 할당이 이루어지도록 하기 위해 (의식적으로) 탈출 분석을 무효화한다.단, 예를 들어 엄청난 수의 어레이 스토어가 캐시와 저장 버퍼를 파괴할 것으로 예상되기 때문에 할당 속도가 매우 빠를 경우 이는 대부분 메모리 벤치마크가 됩니다.(그러나 Peter는 "2.5x"가 아니라 "150ns"라는 결론에 대해 칭찬합니다.그는 이런 일을 하고 있는 것 같다.)

네, 상당히 느립니다.우리는 그것을 하는 코드를 실행하고 있었습니다.현재로서는 사용할 수 있는 메트릭스를 가지고 있지 않지만, 최종적인 결과는 반사를 사용하지 않기 위해 코드를 다시 팩터링해야 한다는 것이었습니다.클래스가 뭔지 알고 있다면 직접 컨스트럭터에게 전화하세요.

doReflection()에서는 Class.forName("misc")으로 인한 오버헤드가 나타납니다.A") (클래스에서 호출된 newInstance()가 아닌 파일 시스템 상의 클래스 경로를 스캔하는 클래스 룩업이 필요합니다.Class.forName("misc")의 경우 통계는 어떻게 될까요?A")는 for-loop 외부에서1회만 실행되므로 루프가 호출될 때마다 실행할 필요는 없습니다.

예. JVM은 컴파일 시 코드를 최적화할 수 없기 때문에 항상 리플렉션에 의한 오브젝트 작성에 시간이 걸립니다.자세한 내용은 Sun/Java Reflection 튜토리얼을 참조하십시오.

다음의 간단한 테스트를 참조해 주세요.

public class TestSpeed {
    public static void main(String[] args) {
        long startTime = System.nanoTime();
        Object instance = new TestSpeed();
        long endTime = System.nanoTime();
        System.out.println(endTime - startTime + "ns");

        startTime = System.nanoTime();
        try {
            Object reflectionInstance = Class.forName("TestSpeed").newInstance();
        } catch (InstantiationException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
        endTime = System.nanoTime();
        System.out.println(endTime - startTime + "ns");
    }
}

Apache Commons BeanUtils 또는 PropertyUtils를 인스펙션(기본적으로 클래스에 대한 메타 데이터를 캐시하여 항상 리플렉션을 사용할 필요는 없음)에 사용할 수 있습니다.

대상 방식이 가볍고 무거운 정도에 따라 다르다고 생각합니다.대상 방식이 매우 가볍다면(getter/setter 등), 1~3배 느릴 수 있고, 대상 방식이 1밀리초 이상 걸리면 퍼포먼스가 매우 비슷합니다.다음은 Java 8과 reflectasm을 사용한 테스트입니다.

public class ReflectionTest extends TestCase {    
    @Test
    public void test_perf() {
        Profiler.run(3, 100000, 3, "m_01 by refelct", () -> Reflection.on(X.class)._new().invoke("m_01")).printResult();    
        Profiler.run(3, 100000, 3, "m_01 direct call", () -> new X().m_01()).printResult();    
        Profiler.run(3, 100000, 3, "m_02 by refelct", () -> Reflection.on(X.class)._new().invoke("m_02")).printResult();    
        Profiler.run(3, 100000, 3, "m_02 direct call", () -> new X().m_02()).printResult();    
        Profiler.run(3, 100000, 3, "m_11 by refelct", () -> Reflection.on(X.class)._new().invoke("m_11")).printResult();    
        Profiler.run(3, 100000, 3, "m_11 direct call", () -> X.m_11()).printResult();    
        Profiler.run(3, 100000, 3, "m_12 by refelct", () -> Reflection.on(X.class)._new().invoke("m_12")).printResult();    
        Profiler.run(3, 100000, 3, "m_12 direct call", () -> X.m_12()).printResult();
    }

    public static class X {
        public long m_01() {
            return m_11();
        }    
        public long m_02() {
            return m_12();
        }    
        public static long m_11() {
            long sum = IntStream.range(0, 10).sum();
            assertEquals(45, sum);
            return sum;
        }    
        public static long m_12() {
            long sum = IntStream.range(0, 10000).sum();
            assertEquals(49995000, sum);
            return sum;
        }
    }
}

완전한 테스트 코드는 GitHub에서 구할 수 있습니다.Reflection Test.java

언급URL : https://stackoverflow.com/questions/435553/java-reflection-performance

반응형