Java 정적 호출은 비 정적 호출보다 비용이 많이 듭니까? 으로든 성능상의 이점이 있습니까? 컴파일러 / VM

어떤 방식 으로든 성능상의 이점이 있습니까? 컴파일러 / VM 특정입니까? 핫스팟을 사용하고 있습니다.



답변

첫째, 성능을 기준으로 정적과 비정 적을 선택해서는 안됩니다.

둘째, 실제로는 아무런 차이가 없습니다. 핫스팟은 한 메서드에 대해 정적 호출을 더 빠르게 만들고 다른 메서드에 대해서는 비 정적 호출을 더 빠르게 만드는 방식으로 최적화하도록 선택할 수 있습니다.

셋째 : 정적과 비정 적을 둘러싼 신화의 대부분은 매우 오래된 JVM (핫스팟이 수행하는 최적화 근처에서 수행하지 않음) 또는 C ++에 대한 기억 된 퀴즈 (동적 호출이 하나 이상의 메모리 액세스를 사용하는 경우)를 기반으로 합니다. 정적 호출보다).


답변

4 년 후 …

좋아,이 질문을 영원히 해결하기 위해 여러 종류의 호출 (가상, 비가 상, 정적)이 서로 어떻게 비교되는지 보여주는 벤치 마크를 작성했습니다.

나는 그것을 ideone 에서 실행했고 이것이 내가 얻은 것입니다.

(반복 횟수가 많을수록 좋습니다.)

    Success time: 3.12 memory: 320576 signal:0
  Name          |  Iterations
    VirtualTest |  128009996
 NonVirtualTest |  301765679
     StaticTest |  352298601
Done.

예상대로 가상 메서드 호출은 가장 느리고 비가 상 메서드 호출은 더 빠르며 정적 메서드 호출은 훨씬 더 빠릅니다.

내가 예상하지 못했던 차이점은 매우 뚜렷했습니다. 가상 메서드 호출은 비가 상 메서드 호출 속도의 절반 이하 로 실행되는 것으로 측정되었으며, 결과적으로 정적 호출보다 전체 15 % 느리게 실행되는 것으로 측정되었습니다 . 이것이 이러한 측정이 보여주는 것입니다. 각 가상, 비가 상 및 정적 메서드 호출에 대해 내 벤치마킹 코드에는 하나의 정수 변수를 증가시키고 부울 변수를 확인하고 참이 아닌 경우 반복하는 추가 상수 오버 헤드가 있기 때문에 실제 차이점은 실제로 약간 더 분명해야합니다.

결과는 CPU마다, JVM마다 다르므로 시도해보고 결과를 확인하십시오.

import java.io.*;

class StaticVsInstanceBenchmark
{
    public static void main( String[] args ) throws Exception
    {
        StaticVsInstanceBenchmark program = new StaticVsInstanceBenchmark();
        program.run();
    }

    static final int DURATION = 1000;

    public void run() throws Exception
    {
        doBenchmark( new VirtualTest( new ClassWithVirtualMethod() ),
                     new NonVirtualTest( new ClassWithNonVirtualMethod() ),
                     new StaticTest() );
    }

    void doBenchmark( Test... tests ) throws Exception
    {
        System.out.println( "  Name          |  Iterations" );
        doBenchmark2( devNull, 1, tests ); //warmup
        doBenchmark2( System.out, DURATION, tests );
        System.out.println( "Done." );
    }

    void doBenchmark2( PrintStream printStream, int duration, Test[] tests ) throws Exception
    {
        for( Test test : tests )
        {
            long iterations = runTest( duration, test );
            printStream.printf( "%15s | %10d\n", test.getClass().getSimpleName(), iterations );
        }
    }

    long runTest( int duration, Test test ) throws Exception
    {
        test.terminate = false;
        test.count = 0;
        Thread thread = new Thread( test );
        thread.start();
        Thread.sleep( duration );
        test.terminate = true;
        thread.join();
        return test.count;
    }

    static abstract class Test implements Runnable
    {
        boolean terminate = false;
        long count = 0;
    }

    static class ClassWithStaticStuff
    {
        static int staticDummy;
        static void staticMethod() { staticDummy++; }
    }

    static class StaticTest extends Test
    {
        @Override
        public void run()
        {
            for( count = 0;  !terminate;  count++ )
            {
                ClassWithStaticStuff.staticMethod();
            }
        }
    }

    static class ClassWithVirtualMethod implements Runnable
    {
        int instanceDummy;
        @Override public void run() { instanceDummy++; }
    }

    static class VirtualTest extends Test
    {
        final Runnable runnable;

        VirtualTest( Runnable runnable )
        {
            this.runnable = runnable;
        }

        @Override
        public void run()
        {
            for( count = 0;  !terminate;  count++ )
            {
                runnable.run();
            }
        }
    }

    static class ClassWithNonVirtualMethod
    {
        int instanceDummy;
        final void nonVirtualMethod() { instanceDummy++; }
    }

    static class NonVirtualTest extends Test
    {
        final ClassWithNonVirtualMethod objectWithNonVirtualMethod;

        NonVirtualTest( ClassWithNonVirtualMethod objectWithNonVirtualMethod )
        {
            this.objectWithNonVirtualMethod = objectWithNonVirtualMethod;
        }

        @Override
        public void run()
        {
            for( count = 0;  !terminate;  count++ )
            {
                objectWithNonVirtualMethod.nonVirtualMethod();
            }
        }
    }

    static final PrintStream devNull = new PrintStream( new OutputStream()
    {
        public void write(int b) {}
    } );
}

이 성능 차이는 매개 변수없는 메서드를 호출하는 것 외에는 아무것도하지 않는 코드에만 적용 할 수 있습니다. 호출 사이에 다른 코드가 있으면 차이점이 희석되며 여기에는 매개 변수 전달이 포함됩니다. 실제로 정적 호출과 비가 상 호출 간의 15 % 차이 는 포인터가 정적 메서드에 전달 될 필요가 없다는 사실에 의해 완전히 설명 this될 수 있습니다. 따라서 서로 다른 종류의 호출 간의 차이가 순 영향을 전혀 갖지 않는 지점까지 희석되도록 호출 사이에 사소한 작업을 수행하는 코드의 양이 상당히 적습니다.

또한 가상 메서드 호출에는 이유가 있습니다. 서비스 목적이 있으며 기본 하드웨어가 제공하는 가장 효율적인 수단을 사용하여 구현됩니다. (CPU 명령 세트.) 비가 상 또는 정적 호출로 대체하여 제거하려는 경우 기능을 에뮬레이션하기 위해 추가 코드를 추가해야하는 경우 결과적으로 발생하는 순 오버 헤드가 제한됩니다. 적지 않고 더 많을 것입니다. 아마도, 훨씬, 훨씬, 헤아릴 수 없을 정도로 훨씬 더 많이.


답변

음, 정적 호출 재정의 할 수 없으며 (따라서 항상 인라인 후보가 됨) nullity 검사가 필요하지 않습니다. 핫스팟은 물론 이러한 장점을 부정 할 수 인스턴스 메서드에 대한 멋진 최적화의 무리를 수행하지만,있는 거 가능한 이유는 왜 정적 호출을 빠르게 할 수있다.

그러나 그것은 당신의 디자인에 영향을주지 않아야합니다-가장 읽기 쉽고 자연스러운 방법으로 코드-당신이 정당한 이유가있는 경우에만 이런 종류의 마이크로 최적화에 대해 걱정하십시오 (거의 절대 그렇게 하지 않을 것입니다).


답변

컴파일러 / VM에 따라 다릅니다.

  • 이론적 으로 정적 호출은 가상 함수 조회를 수행 할 필요가 없기 때문에 약간 더 효율적으로 만들 수 있으며 숨겨진 “this”매개 변수의 오버 헤드도 피할 수 있습니다.
  • 실제로 많은 컴파일러가이를 최적화합니다.

따라서이를 애플리케이션에서 진정으로 중요한 성능 문제로 식별하지 않는 한 신경 쓸 가치가 없을 것입니다. 조기 최적화는 모든 악의 근원입니다.

그러나 나는 이 최적화는 다음과 같은 상황에서 상당한 성능 향상을 제공 볼 수 :

  • 메모리 액세스없이 매우 간단한 수학적 계산을 수행하는 방법
  • 엄격한 내부 루프에서 초당 수백만 번 호출되는 메서드
  • 모든 성능이 중요한 CPU 바운드 애플리케이션

위의 내용이 적용되는 경우 테스트 해 볼 가치가 있습니다.

정적 메서드를 사용하는 또 다른 좋은 이유가 있습니다 (잠재적으로 더 중요합니다!). 메서드에 실제로 정적 의미가있는 경우 (즉, 논리적으로 클래스의 지정된 인스턴스에 연결되지 않은 경우) 정적 메서드를 만드는 것이 합리적입니다. 이 사실을 반영합니다. 숙련 된 Java 프로그래머는 static modifier를 발견하고 즉시 “아하!이 메서드는 정적이므로 인스턴스가 필요하지 않으며 아마도 인스턴스 특정 상태를 조작하지 않습니다”라고 생각할 것입니다. 따라서 방법의 정적 특성을 효과적으로 전달하게 될 것입니다 ….


답변

이전 포스터에서 말했듯이 : 이것은 조기 최적화처럼 보입니다.

그러나 한 가지 차이점이 있습니다 (비 정적 호출에는 피연산자 스택에 피 호출자 객체를 추가로 푸시해야한다는 사실과 일부) :

정적 메서드는 재정의 할 수 없으므로 런타임에 정적 메서드 호출에 대한 가상 조회가 없습니다 . 이로 인해 일부 상황에서 눈에 띄는 차이가 발생할 수 있습니다.

바이트 코드 레벨에서의 차이는 비 정적 메소드 호출을 통해 수행된다는 점이다 INVOKEVIRTUAL, INVOKEINTERFACE또는 INVOKESPECIAL정적 메소드 호출을 통해 수행되는 동안 INVOKESTATIC.


답변

정적 호출과 비 정적 호출의 성능 차이가 애플리케이션에 영향을 미칠 가능성은 거의 없습니다. “조기 최적화는 모든 악의 근원”임을 기억하십시오.


답변

7 년 후 …

Mike Nakis가 발견 한 결과는 핫스팟 최적화와 관련된 몇 가지 일반적인 문제를 해결하지 못하기 때문에 큰 확신이 없습니다. JMH를 사용하여 벤치 마크를 계측했으며 인스턴스 메서드의 오버 헤드가 정적 호출에 비해 내 컴퓨터에서 약 0.75 %라는 것을 발견했습니다. 지연 시간에 가장 민감한 작업을 제외하고는 오버 헤드가 낮다는 점을 감안할 때 애플리케이션 설계에서 가장 큰 문제가 아니라고 생각합니다. 내 JMH 벤치 마크의 요약 결과는 다음과 같습니다.

java -jar target/benchmark.jar

# -- snip --

Benchmark                        Mode  Cnt          Score         Error  Units
MyBenchmark.testInstanceMethod  thrpt  200  414036562.933 ± 2198178.163  ops/s
MyBenchmark.testStaticMethod    thrpt  200  417194553.496 ± 1055872.594  ops/s

여기 Github에서 코드를 볼 수 있습니다.

https://github.com/nfisher/svsi

벤치 마크 자체는 매우 간단하지만 데드 코드 제거 및 지속적인 폴딩을 최소화하는 것을 목표로합니다. 내가 놓쳤거나 간과 한 다른 최적화가있을 수 있으며 이러한 결과는 JVM 릴리스 및 OS에 따라 다를 수 있습니다.

package ca.junctionbox.svsi;

import org.openjdk.jmh.annotations.Benchmark;
import org.openjdk.jmh.annotations.Scope;
import org.openjdk.jmh.annotations.State;
import org.openjdk.jmh.infra.Blackhole;

class InstanceSum {
    public int sum(final int a, final int b) {
        return a + b;
    }
}

class StaticSum {
    public static int sum(final int a, final int b) {
        return a + b;
    }
}

public class MyBenchmark {
    private static final InstanceSum impl = new InstanceSum();

    @State(Scope.Thread)
    public static class Input {
        public int a = 1;
        public int b = 2;
    }

    @Benchmark
    public void testStaticMethod(Input i, Blackhole blackhole) {
        int sum = StaticSum.sum(i.a, i.b);
        blackhole.consume(sum);
    }

    @Benchmark
    public void testInstanceMethod(Input i, Blackhole blackhole) {
        int sum = impl.sum(i.a, i.b);
        blackhole.consume(sum);
    }
}