programing

Java 스태틱콜은 비 스태틱콜보다 비싼가요, 아니면 저렴한가요?

copysource 2022. 9. 29. 22:41
반응형

Java 스태틱콜은 비 스태틱콜보다 비싼가요, 아니면 저렴한가요?

어떤 식으로든 퍼포먼스상의 이점이 있습니까?컴파일러/VM에 고유합니까?핫스팟을 사용하고 있습니다.

첫째, 퍼포먼스에 따라 스태틱과 비 스태틱 중 하나를 선택해서는 안 됩니다.

둘째, 실제로는 아무런 차이가 없습니다.핫스팟은 어떤 메서드에 대해서는 스태틱콜을 고속화하고 다른 메서드에 대해서는 비 스태틱콜을 고속화하는 방법으로 최적화할 수 있습니다.

셋째, 스태틱과 비 스태틱을 둘러싼 통념의 대부분은 매우 오래된 JVM(Hotspot이 최적화하는 방법과는 거리가 멀었다) 또는 C++(동적 콜은 스태틱 콜보다 메모리액세스를 1개 더 사용한다)에 대해 기억되고 있는 상식에 근거하고 있습니다.

4년 후...

네, 이 문제를 영원히 해결하고자 여러 종류의 콜(가상, 비가상, 정적)이 서로 어떻게 비교되는지를 보여주는 벤치마크를 작성했습니다.

ideone으로 실행해 봤는데, 이렇게 나왔어요.

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

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

예상대로 가상 메서드콜은 가장 느리고 비가상 메서드콜은 더 빠르며 스태틱메서드콜은 더 빠릅니다

내가 예상하지 못한 것은 차이가 그렇게 뚜렷하게 드러날 것이라는 것이다.가상 메서드콜은 비가상 메서드콜의 절반 이하로 실행되도록 측정되었으며, 그 결과 스태틱콜보다 15%나 느리게 실행되도록 측정되었습니다.이러한 측정치를 통해 알 수 있습니다.가상, 비가상 및 스태틱 메서드콜마다 벤치마크코드에는 1개의 정수변수 증가, 부울변수 체크, true가 아닌 경우 루프하는 추가 고정 오버헤드가 있기 때문에 실제 차이는 조금 더 뚜렷해야 합니다.

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 명령 세트).이러한 콜을 비가상 콜 또는 스태틱콜로 대체하여 제거하고 싶은 경우 기능을 에뮬레이트하기 위해 추가 코드를 대량으로 추가해야 할 경우 결과적으로 발생하는 순 오버헤드는 줄어들지 않고 증가하게 됩니다.아주, 아주, 아주, 엄청나게, 더 많이, 더 많이.

스태틱 은 덮어쓸 수 없습니다(인라인의 후보도 항상 마찬가지입니다).늘 체크는 필요 없습니다.HotSpot은 인스턴스 메서드에 대해 많은 쿨한 최적화를 수행하므로 이러한 이점이 충분히 무효가 될 수 있지만, 스태틱 콜이 더 빠른 이유일 수 있습니다.

단, 가장 읽기 쉽고 자연스러운 코드인 설계에 영향을 주지 않으며, 원인이 되는 경우(거의 발생하지 않는 경우)에만 이러한 미세 최적화를 걱정할 수 있습니다.

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

  • 이론적으로 스태틱콜은 가상함수 룩업을 실행할 필요가 없기 때문에 약간 더 효율적이며 숨겨진 "this" 파라미터의 오버헤드를 회피할 수도 있습니다.
  • 실제로 많은 컴파일러가 이를 최적화합니다.

따라서 어플리케이션에서 이것이 정말로 중요한 성능 문제로 인식되지 않는 한 신경 쓸 필요가 없습니다.섣부른 최적화는 모든 악의 근원...

, 이 최적화로 인해 다음과 같은 상황에서 성능이 크게 향상되었습니다.

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

위의 내용이 적용된다면 시험해 볼 가치가 있을지도 모릅니다.

정적 메서드를 사용하는 다른 좋은 이유(그리고 잠재적으로 더 중요!)가 하나 더 있습니다. 만약 메서드가 실제로 정적 의미론(즉, 논리적으로 클래스의 주어진 인스턴스에 연결되지 않음)을 가지고 있다면, 이 사실을 반영하기 위해 정적으로 만드는 것이 이치에 맞습니다.경험이 풍부한 Java 프로그래머는 정적 수식어를 알아차리고 즉시 "아하! 이 메서드는 정적이어서 인스턴스가 필요하지 않고 인스턴스 고유의 상태를 조작하지 않을 것"이라고 생각할 것입니다.따라서 메서드의 정적 특성을 효과적으로 전달할 수 있습니다.

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가지 차이점이 있습니다(비정적 호출에서는 오퍼랜드스택에 착신측 오브젝트를 추가로 푸시할 필요가 있는 부분).

정적 메서드는 덮어쓸 수 없으므로 정적 메서드 호출에 대한 실행 시 가상 룩업은 없습니다.이로 인해 상황에 따라 눈에 띄는 차이가 발생할 수 있습니다.

바이트 코드레벨의 차이점은 비스타틱 방식 콜은 를 통해 이루어지며 스태틱 방식 콜은 를 통해 이루어집니다.

스태틱 콜과 스태틱하지 않은 콜의 퍼포먼스에 차이가 있을 가능성은 거의 없습니다."최신 최적화는 모든 악의 근원"이라는 점을 기억하십시오.

어떤 방법이 정적인지 판단하기 위해 성능 측면은 무관해야 합니다.퍼포먼스에 문제가 있는 경우, 많은 방법을 정적으로 설정해도 문제가 해결되지 않습니다.즉, 스태틱 방식은 인스턴스 방식보다 속도가 느리지 않으며 대부분의 경우 속도가 약간 빠릅니다.

1) 정적 메서드는 다형이 아니기 때문에 JVM은 실행할 실제 코드를 찾는 데 필요한 결정을 내릴 필요가 없습니다.핫스팟은 구현 사이트가1개뿐인 인스턴스 메서드콜을 최적화하기 때문에 이는 핫스팟 시대의 중요한 포인트입니다.

2) 또 다른 미묘한 차이는 정적 방법에는 분명히 "이" 참조가 없다는 것입니다.이로 인해 스택프레임의 크기는 시그니처와 본문이 같은 인스턴스 방식의 슬롯보다1 슬롯 작아집니다("이"는 바이트 코드레벨의 로컬 변수의 슬롯0 에 배치됩니다만, 스태틱 방식의 경우 슬롯0 이 메서드의 첫 번째 파라미터에 사용됩니다).

차이가 있을 수 있으며, 어떤 특정 코드에 대해서도 어떤 방식으로든 적용될 수 있으며 JVM의 마이너 릴리스에서도 변경될 수 있습니다.

는 97%의 소규모 효율성가장 중요한 부분입니다.

이론적으로, 더 저렴합니다.

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

하지만, 저는 이것을 테스트하지 않았습니다.

Jon이 지적했듯이 정적 메서드는 재정의할 수 없기 때문에 정적 메서드를 호출하는 이 인스턴스 메서드를 호출하는 보다 충분히 순진한 Java 런타임에서 더 순진한 Java 런타임에 수 있습니다.

그러나 몇 나노초를 절약하기 위해 설계를 망치는 것에 관심을 갖는다고 가정하더라도, 이는 또 다른 질문을 불러일으킬 뿐입니다: 자신을 재정의하는 방법이 필요합니까?코드를 변경하여 인스턴스 메서드를 스태틱한 메서드로 만들어 나노초를 절약하고 그 위에 자신의 디스패처를 구현하면 자바 런타임에 이미 내장되어 있는 것보다 효율이 떨어질 것이 거의 확실합니다.

다른 훌륭한 답변에 덧붙이겠습니다.그것은, 플로우에 의해서도 다릅니다.예를 들면 다음과 같습니다.

Public class MyDao {

   private String sql = "select * from MY_ITEM";

   public List<MyItem> getAllItems() {
       springJdbcTemplate.query(sql, new 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);
   };
};

언급URL : https://stackoverflow.com/questions/3805155/are-java-static-calls-more-or-less-expensive-than-non-static-calls

반응형