Java 정적 호출은 비 정적 호출보다 비용이 많이 듭니까?


답변:


74

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

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

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


1
이것만으로 정적 인 방법을 선호해서는 안된다는 것이 절대적으로 맞습니다. 그러나 정적 메서드가 디자인에 잘 맞는 경우 인스턴스 메서드보다 빠르지는 않더라도 적어도 속도가 빠르며 성능 측면에서 배제해서는 안된다는 것을 아는 것이 유용합니다.
Will

2
@AaronDigulla -.- 내가 지금 최적화하고 있기 때문에 여기에 왔다고 말하면 어떨까요? OP가 조기에 최적화하기를 원한다고 가정했지만이 사이트가 좀 글로벌하다는 것을 알고 있습니다. 맞습니까? 무례하고 싶지는 않지만 다음에 이런 일을하지 마세요.
Dalibor Filus

1
@DaliborFilus 균형을 찾아야합니다. 정적 메서드를 사용하면 모든 종류의 문제가 발생하므로 특히 수행중인 작업을 모르는 경우에는 피해야합니다. 둘째, 대부분의 "느린"코드는 선택 언어가 느리기 때문이 아니라 (나쁜) 설계 때문입니다. 코드가 느리다면 정적 메서드는 아무것도하지 않는 호출 메서드 아니면 저장하지 않을 것입니다 . 대부분의 경우, 코드 방법은 호출 오버 헤드 난쟁이됩니다.
Aaron Digulla 2017

6
비추천. 이것은 질문에 대한 답이 아닙니다. 성능상의 이점에 대해 질문했습니다. 디자인 원칙에 대한 의견을 묻지 않았습니다.
Colm Bhandal

4
내가 앵무새에게 "조기 opimisation은 모든 악의 근원"이라고 말하도록 훈련했다면, 나는 앵무새만큼 성능에 대해 많이 아는 사람들로부터 1000 표를받을 것입니다.
rghome

62

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 명령 세트.) 비가 상 또는 정적 호출로 대체하여 제거하려는 경우 기능을 에뮬레이션하기 위해 추가 코드를 추가해야하는 경우 결과적으로 발생하는 순 오버 헤드가 제한됩니다. 적지 않고 더 많을 것입니다. 아마도, 훨씬, 훨씬, 헤아릴 수 없을 정도로 훨씬 더 많이.


7
'가상'은 C ++ 용어입니다. Java에는 가상 메소드가 없습니다. 런타임 다형성 인 일반 메서드와 그렇지 않은 정적 또는 최종 메서드가 있습니다.
Zhenya

16
@levgen 예, 언어에 대한 공식적인 고차원 적 개요만큼 관점이 좁은 사람에게는 말 그대로입니다. 그러나 물론 높은 수준의 개념은 자바가 존재하기 오래 전에 발명 된 잘 확립 된 낮은 수준의 메커니즘을 사용하여 구현되며 가상 메서드도 그중 하나입니다. 당신은 후드 아래 단지 작은 살펴 경우, 즉시이 그렇다고 볼 수 있습니다 : docs.oracle.com/javase/specs/jvms/se7/html/...
마이크 Nakis

13
조기 최적화에 대한 추측없이 질문에 답 해주셔서 감사합니다. 좋은 대답입니다.
vegemite4me

3
네, 정확히 제가 의미 한 바입니다. 어쨌든 내 컴퓨터에서 테스트를 실행했습니다. 이러한 벤치 마크에서 예상 할 수있는 지터를 제외하고는 VirtualTest | 488846733 -- NonVirtualTest | 480530022 -- StaticTest | 484353198OpenJDK 설치 에서 속도 차이가 전혀 없습니다 . FTR : final수정자를 제거해도 마찬가지 입니다. Btw. 나는 terminate현장 을 만들어야했다 volatile. 그렇지 않으면 시험이 끝나지 않았다.
Marten

4
참고로 Android 6을 실행하는 Nexus 5에서 다소 놀라운 결과를 얻었습니다 VirtualTest | 12451872 -- NonVirtualTest | 12089542 -- StaticTest | 8181170.. 내 노트북의 OpenJDK가 40 배 더 많은 반복을 수행 할뿐만 아니라 정적 테스트의 처리량은 항상 약 30 % 적습니다. Android 4.4 태블릿에서 예상되는 결과가 나오기 때문에 이것은 ART 특정 현상 일 수 있습니다.VirtualTest | 138183740 -- NonVirtualTest | 142268636 -- StaticTest | 161388933
Marten

46

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

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


정적 호출이 더 빠른 이유 일 수 있습니다. 그 이유를 설명해 주시겠습니까?
JavaTechnical 2013

6
@JavaTechnical는 - 결코 무시 (각 시간을 사용하는 구현에서 작업을 필요로하지 않는 수단 대답은 그 이유를 설명하지 하고 당신은 인라인 수) 당신은 당신이에 메소드를 호출하고 있는지 여부를 확인 할 필요가 없습니다 null 참조.
Jon Skeet 2013

6
@JavaTechnical : 이해가 안 돼요. 인라인 기회와 함께 정적 메서드에 대해 계산 / 확인할 필요가없는 항목을 방금 제공했습니다. 일을하지 않는 것은 입니다 성능 혜택을 누릴 수 있습니다. 이해해야 할 것은 무엇입니까?
Jon Skeet 2013

정적 변수가 비 정적 변수보다 빠르게 검색됩니까?
JavaTechnical jul.

1
@JavaTechnical : 수행 할 nullity 검사는 없습니다.하지만 JIT 컴파일러가 해당 검사 (컨텍스트 별)를 제거 할 수 있다면 큰 차이를 기대하지 않을 것입니다. 메모리가 캐시에 있는지 여부와 같은 것이 훨씬 더 중요합니다.
Jon Skeet 2013

18

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

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

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

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

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

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

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


14

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

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

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

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


2
그러나 개인 인스턴스 메소드 invokespecial는 가상이 아니기 때문에 (적어도 일반적으로) 사용하여 호출됩니다 .
Mark Peters

아, 흥미 롭 네요. 생성자 밖에 생각 나지 않았어요. 그래서 생략 했어요! 감사! (업데이트 답변)
aioobe

2
하나의 유형이 인스턴스화되면 JVM이 최적화됩니다. B가 A를 확장하고 B의 인스턴스가 인스턴스화되지 않은 경우 A에 대한 메서드 호출에는 가상 테이블 조회가 필요하지 않습니다.
Steve Kuo

13

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



""조기 최적화는 모든 악의 근원 ""이 무엇인지 자세히 설명해 주시겠습니까?
user2121 2016

질문은 "어떤 방식 으로든 성능상의 이점이 있습니까?"였으며, 이것이 바로 그 질문에 대한 답입니다.
DJClayworth 19.01.28

13

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

1
순수한 학문적 관심이 여기에 있습니다. 이러한 종류의 마이크로 최적화가 ops/s주로 ART 환경 (예 : 메모리 사용량, .oat 파일 크기 감소 등) 이외의 측정 항목에 미칠 수있는 잠재적 인 이점에 대해 궁금 합니다. 이러한 다른 측정 항목을 벤치마킹하기 위해 시도 할 수있는 비교적 간단한 도구 / 방법을 알고 있습니까?
라이언 토마스

Hotspot은 클래스 경로에 InstanceSum에 대한 확장이 없음을 알아냅니다. InstanceSum을 확장하고 메서드를 재정의하는 다른 클래스를 추가해보십시오.
milan

12

메서드가 정적이어야하는지 여부를 결정하려면 성능 측면이 무관해야합니다. 성능 문제가있는 경우 많은 메서드를 정적으로 만드는 것은 하루를 절약 할 수 없습니다. 즉 정적 방법이 거의 확실하다 말했다 하지 느린 대부분의 경우, 인스턴스 방법보다 소폭 빠른 :

1.) 정적 메소드는 다형성이 아니므로 JVM은 실행할 실제 코드를 찾기 위해 결정해야 할 결정이 적습니다. 핫스팟은 구현 사이트가 하나만있는 인스턴스 메서드 호출을 최적화하여 동일한 작업을 수행하기 때문에 이것은 핫스팟 시대의 논쟁 점입니다.

2.) 또 다른 미묘한 차이점은 정적 메서드에 "this"참조가 없다는 것입니다. 이로 인해 동일한 서명과 본문을 가진 인스턴스 메서드보다 한 슬롯 더 작은 스택 프레임이 생성됩니다 ( "this"는 바이트 코드 수준에서 로컬 변수의 슬롯 0에 배치되는 반면, 정적 메서드의 경우 슬롯 0이 첫 번째에 사용됨). 방법의 매개 변수).


5

차이가있을 수 있으며 특정 코드에 대해 어느 쪽이든 갈 수 있으며 JVM의 부 릴리스에서도 변경 될 수 있습니다.

이것은 당신이 잊어야 할 작은 효율성의 97 % 중 가장 확실히 일부입니다 . .


2
잘못된. 아무것도 짐작할 수 없습니다. UI가 얼마나 "간단"한지에 큰 차이를 만들 수있는 프런트 엔드 UI에 필요한 엄격한 루프 일 수 있습니다. 예를 들어 TableView수백만 개의 레코드를 검색 합니다.
trilogy

0

이론적으로는 저렴합니다.

정적 초기화는 객체의 인스턴스를 생성하더라도 수행되는 반면 정적 메서드는 생성자에서 일반적으로 수행되는 초기화를 수행하지 않습니다.

그러나 나는 이것을 테스트하지 않았습니다.


1
@아르 자형. Bemrose, 정적 초기화는이 질문과 어떤 관련이 있습니까?
Kirk Woll

@Kirk Woll : 정적 초기화는 클래스가 처음 참조 될 때 수행되므로 첫 번째 정적 메서드 호출 이전을 포함합니다.
Powerlord

@아르 자형. Bemrose는 시작하기 위해 클래스를 VM에로드하는 것입니다. 비 sequitor, IMO처럼 보입니다.
Kirk Woll

0

존 노트로, 정적 방법은 너무 간단하게 재정의 할 수 없습니다 호출 충분히 순진 자바 런타임에 - -보다 빠른 될 수있는 정적 메서드를 호출 인스턴스 방법.

그러나 몇 나노초를 절약하기 위해 설계를 망쳐 놓는 지점에 있다고 가정하더라도 다른 질문이 제기됩니다. 자신을 재정의하는 방법이 필요합니까? 인스턴스 메서드를 정적 메서드로 변경하여 여기 저기 나노초를 절약 한 다음 그 위에 자신의 디스패처를 구현하면 빌드 된 것보다 효율성이 떨어질 것입니다. 이미 Java 런타임에.


-2

예를 들어 흐름에 따라 달라지는 다른 훌륭한 답변에 추가하고 싶습니다.

Public class MyDao {

   private String sql = "select * from MY_ITEM";

   public List<MyItem> getAllItems() {
       springJdbcTemplate.query(sql, new MyRowMapper());
   };
};

각 호출마다 새 MyRowMapper 개체를 만드는 데주의하십시오.
대신 여기에 정적 필드를 사용하는 것이 좋습니다.

Public class MyDao {

   private static RowMapper myRowMapper = new MyRowMapper();
   private String sql = "select * from MY_ITEM";

   public List<MyItem> getAllItems() {
       springJdbcTemplate.query(sql, myRowMapper);
   };
};
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.