Java에서 instanceof를 사용하면 성능에 미치는 영향


Answers

접근

다른 구현을 평가 하는 벤치 마크 프로그램 을 작성 했습니다 .

  1. instanceof 구현 (참조)
  2. 추상 클래스를 통해 지향 된 객체와 @Override 테스트 메소드를 @Override 합니다.
  3. 자체 유형 구현 사용
  4. getClass() == _.class 구현

jmh 를 사용하여 100 회의 워밍업 호출, 1000 회 반복 측정 및 10 회의 포크로 벤치 마크를 실행했습니다. 따라서 각 옵션은 10,000 회 측정되었습니다. MacOS Pro에서 macOS 10.12.4 및 Java 1.8을 사용하여 전체 벤치 마크를 실행하는 데 12 시간 18 분 57 초가 소요됩니다. 벤치 마크는 각 옵션의 평균 시간을 측정합니다. 자세한 내용은 GitHub의 구현을 참조하십시오.

완전성을 위해 : 이 답변 의 이전 버전과 벤치 마크가 있습니다.

결과

| Operation  | Runtime in nanoseconds per operation | Relative to instanceof |
|------------|--------------------------------------|------------------------|
| INSTANCEOF | 39,598 ± 0,022 ns/op                 | 100,00 %               |
| GETCLASS   | 39,687 ± 0,021 ns/op                 | 100,22 %               |
| TYPE       | 46,295 ± 0,026 ns/op                 | 116,91 %               |
| OO         | 48,078 ± 0,026 ns/op                 | 121,42 %               |

tl; dr

Java 1.8에서는 getClass() 가 매우 가까웠지만 instanceof 가 가장 빠른 접근 방식입니다.

Question

나는 응용 프로그램에서 일하고 있으며 한 가지 설계 접근법은 instanceof 연산자를 상당히 많이 사용하는 것과 관련이 있습니다. OO 디자인은 일반적으로 instanceof 사용을 피하려고 시도하지만, 이는 다른 이야기이며이 질문은 단순히 성능과 관련이 있습니다. 성능에 영향이 있는지 궁금합니다. 그것은 == 와 똑같은 속도입니까?

예를 들어, 10 개의 하위 클래스가있는 기본 클래스가 있습니다. 기본 클래스를 취하는 단일 함수에서 클래스가 하위 클래스의 인스턴스인지 확인하고 루틴을 수행합니다.

내가 그것을 해결하기 위해 생각한 다른 방법 중 하나는 대신 "형식 ID"정수 기본을 사용하고 하위 클래스의 범주를 나타내는 데 비트 마스크를 사용한 다음 서브 클래스의 "유형 ID"를 비트 마스크 비교를 수행하여 카테고리를 나타내는 정수 마스크.

instanceof 가 JVM에 의해 최적화 된 것보다 빠릅니다. Java에 충실하고 싶지만 앱의 성능이 중요합니다. 이 길을 걷던 누군가가 약간의 충고를 할 수 있으면 멋질 것입니다. 지나치게 질식 시키거나 잘못된 것을 집중하여 최적화할까요?




성능 영향을 결정할 항목은 다음과 같습니다.

  1. instanceof 연산자가 true를 반환 할 수있는 가능한 클래스의 수입니다.
  2. 데이터 배포 - 첫 번째 또는 두 번째 시도에서 대부분 해결되는 작업의 대부분입니까? 먼저 진정한 작업을 반환 할 가능성이 가장 높습니다.
  3. 전개 환경. Sun Solaris VM에서 실행하는 것은 Sun의 Windows JVM과 크게 다릅니다. Solaris는 기본적으로 '서버'모드로 실행되지만 Windows는 클라이언트 모드로 실행됩니다. Solaris에서의 JIT 최적화는 모든 메소드 액세스를 동일하게 할 수 있습니다.

나는 4 개의 다른 파견 방법을위한 마이크로 벤치 마크를 만들었다. Solaris의 결과는 다음과 같습니다. 그 수가 적 으면 빠릅니다 :

InstanceOf 3156
class== 2925 
OO 3083 
Id 3067 



나는 동일한 질문을 가지고있다. 그러나 나는 나의 것과 유사한 유스 케이스를위한 '성능 메트릭'을 찾지 못했기 때문에, 나는 좀 더 많은 샘플 코드를 작성했다. 내 하드웨어와 Java 6 & 7에서는 instanceof와 10mln iterations의 스위치가 다릅니다.

for 10 child classes - instanceof: 1200ms vs switch: 470ms
for 5 child classes  - instanceof:  375ms vs switch: 204ms

따라서 instanceof는 정말 느리고 특히 if-else-if 명령문의 수가 많습니다. 그러나 실제 응용 프로그램에서는 차이가 무시할 수 있습니다.

import java.util.Date;

public class InstanceOfVsEnum {

    public static int c1, c2, c3, c4, c5, c6, c7, c8, c9, cA;

    public static class Handler {
        public enum Type { Type1, Type2, Type3, Type4, Type5, Type6, Type7, Type8, Type9, TypeA }
        protected Handler(Type type) { this.type = type; }
        public final Type type;

        public static void addHandlerInstanceOf(Handler h) {
            if( h instanceof H1) { c1++; }
            else if( h instanceof H2) { c2++; }
            else if( h instanceof H3) { c3++; }
            else if( h instanceof H4) { c4++; }
            else if( h instanceof H5) { c5++; }
            else if( h instanceof H6) { c6++; }
            else if( h instanceof H7) { c7++; }
            else if( h instanceof H8) { c8++; }
            else if( h instanceof H9) { c9++; }
            else if( h instanceof HA) { cA++; }
        }

        public static void addHandlerSwitch(Handler h) {
            switch( h.type ) {
                case Type1: c1++; break;
                case Type2: c2++; break;
                case Type3: c3++; break;
                case Type4: c4++; break;
                case Type5: c5++; break;
                case Type6: c6++; break;
                case Type7: c7++; break;
                case Type8: c8++; break;
                case Type9: c9++; break;
                case TypeA: cA++; break;
            }
        }
    }

    public static class H1 extends Handler { public H1() { super(Type.Type1); } }
    public static class H2 extends Handler { public H2() { super(Type.Type2); } }
    public static class H3 extends Handler { public H3() { super(Type.Type3); } }
    public static class H4 extends Handler { public H4() { super(Type.Type4); } }
    public static class H5 extends Handler { public H5() { super(Type.Type5); } }
    public static class H6 extends Handler { public H6() { super(Type.Type6); } }
    public static class H7 extends Handler { public H7() { super(Type.Type7); } }
    public static class H8 extends Handler { public H8() { super(Type.Type8); } }
    public static class H9 extends Handler { public H9() { super(Type.Type9); } }
    public static class HA extends Handler { public HA() { super(Type.TypeA); } }

    final static int cCycles = 10000000;

    public static void main(String[] args) {
        H1 h1 = new H1();
        H2 h2 = new H2();
        H3 h3 = new H3();
        H4 h4 = new H4();
        H5 h5 = new H5();
        H6 h6 = new H6();
        H7 h7 = new H7();
        H8 h8 = new H8();
        H9 h9 = new H9();
        HA hA = new HA();

        Date dtStart = new Date();
        for( int i = 0; i < cCycles; i++ ) {
            Handler.addHandlerInstanceOf(h1);
            Handler.addHandlerInstanceOf(h2);
            Handler.addHandlerInstanceOf(h3);
            Handler.addHandlerInstanceOf(h4);
            Handler.addHandlerInstanceOf(h5);
            Handler.addHandlerInstanceOf(h6);
            Handler.addHandlerInstanceOf(h7);
            Handler.addHandlerInstanceOf(h8);
            Handler.addHandlerInstanceOf(h9);
            Handler.addHandlerInstanceOf(hA);
        }
        System.out.println("Instance of - " + (new Date().getTime() - dtStart.getTime()));

        dtStart = new Date();
        for( int i = 0; i < cCycles; i++ ) {
            Handler.addHandlerSwitch(h1);
            Handler.addHandlerSwitch(h2);
            Handler.addHandlerSwitch(h3);
            Handler.addHandlerSwitch(h4);
            Handler.addHandlerSwitch(h5);
            Handler.addHandlerSwitch(h6);
            Handler.addHandlerSwitch(h7);
            Handler.addHandlerSwitch(h8);
            Handler.addHandlerSwitch(h9);
            Handler.addHandlerSwitch(hA);
        }
        System.out.println("Switch of - " + (new Date().getTime() - dtStart.getTime()));
    }
}



instanceof는 아마 대부분의 실제 구현에서 단순한 같음보다 비용이 많이 드는 것입니다 (즉, instanceof가 실제로 필요한 경우). 모든 초급 교과서와 같은 일반적인 방법을 재정 의하여 문제를 해결할 수 없습니다. 위의 Demian 참조).

왜 그런가요? 어쩌면 일어날 일은 몇 가지 인터페이스를 가지고 있으며 인터페이스 x, y, z라고하는 일부 기능을 제공하고 일부 인터페이스를 구현할 수도있는 객체도 있습니다. 직접적으로. 예를 들어 다음과 같이 말합니다.

w는 x를 확장한다.

A를 구현

B는 A를 연장한다.

C는 B를 확장하고, y를 구현합니다.

D는 C를 확장하고, z를 구현합니다.

개체 D를 인스턴스 D로 처리한다고 가정 해 보겠습니다. Computing (d instanceof x)은 d.getClass ()를 취하고 인터페이스가 == ~ x인지를 알기 위해 구현 한 인터페이스를 반복해야하고 그렇지 않다면 모든 조상을 재귀 적으로 반복해야한다. 우리의 경우, 당신이 그 나무에 대한 폭 넓은 탐사를한다면 적어도 8 번의 비교를 할 것입니다. y와 z가 아무 것도 연장하지 않는다고 가정하면 ...

실제 파생 트리의 복잡성은 더 높아질 수 있습니다. 경우에 따라 JIT는 가능한 모든 경우에 x를 확장하는 무언가의 인스턴스가되는 것으로 미리 d를 해결할 수 있다면 대부분의 경우이를 최적화 할 수 있습니다. 현실적으로 그러나, 당신은 그 나무를 거의 통과 할 것입니다.

그게 문제가된다면, 대신 처리기 맵을 사용하여 객체의 구체적인 클래스를 처리를 수행하는 클로저에 연결하는 것이 좋습니다. 직접 매핑을 위해 트리 탐색 단계를 제거합니다. 그러나 C.class에 대한 핸들러를 설정하면 위의 오브젝트 d가 인식되지 않습니다.

여기 내 2 센트, 그들이 도와주기를 바랍니다 ...




'instanceof'는 실제로 + 또는 -와 같은 연산자이며, 자체 JVM 바이트 코드 명령어가 있다고 생각합니다. 그것은 충분히 빠르다.

객체가 일부 집속의 인스턴스인지 테스트 할 스위치가있는 경우 디자인을 다시 작성해야 할 수도 있습니다. 하위 클래스 별 동작을 하위 클래스 자체로 푸시 다운하는 것이 좋습니다.




특정 JVM이 인스턴스를 구현하는 방법을 말하는 것은 어렵지만 대부분의 경우 객체는 구조체 및 클래스와 유사하며 모든 객체 구조체는 인스턴스 인 클래스 구조체에 대한 포인터를 갖습니다. 그래서 실제로 instanceof

if (o instanceof java.lang.String)

다음 C 코드만큼 빠름

if (objectStruct->iAmInstanceOf == &java_lang_String_class)

JIT 컴파일러가 제자리에 있고 괜찮은 일을한다고 가정합니다.

이 포인터를 액세스하는 것을 고려하면 포인터가 가리키는 특정 오프셋에서 포인터를 가져 와서 이것을 다른 포인터와 비교하면 (기본적으로 32 비트 수가 같은지 테스트하는 것과 동일 함), 실제로 연산이 수행 될 수 있다고 말하고 싶습니다. 매우 빠르다.

그것은 JVM에 많은 영향을 미칠 필요는 없습니다. 그러나 이것이 코드에서 병목 현상으로 판명되면 JVM 구현이 다소 불량하다고 생각할 것입니다. JIT 컴파일러가없고 코드 만 해석하는 경우조차 사실상 즉시 인스턴스 테스트를 수행 할 수 있어야합니다.




Demian과 Paul은 좋은 점을 언급합니다. 그러나 실제로 실행할 코드 배치는 데이터 사용 방법에 달려 있습니다.

나는 여러 가지 방법으로 사용할 수있는 작은 데이터 객체를 좋아합니다. 재정의 (다형성) 접근법을 따르는 경우 객체는 "편도"로만 사용할 수 있습니다.

이것은 패턴이 들어오는 곳입니다 ...

방문자 패턴과 같이 이중 디스패치를 ​​사용하여 각 객체가 자신을 지나쳐 "호출"하도록 요청할 수 있습니다. 그러면 객체의 유형이 해결됩니다. 그러나 (다시) 모든 가능한 하위 유형을 "할 수있는"수업이 필요합니다.

전략 패턴을 사용하는 것이 좋습니다. 여기서 전략 패턴을 사용하여 처리하려는 각 하위 유형에 대한 전략을 등록 할 수 있습니다. 다음과 같은 것. 정확한 유형 일치에만 도움이되지만 확장 가능하다는 장점이 있습니다. 타사 기고가가 자신의 유형과 처리기를 추가 할 수 있습니다. (이는 새로운 번들을 추가 할 수있는 OSGi와 같은 동적 프레임 워크에 유용합니다)

다행히도 이것은 다른 아이디어에 영감을 줄 것입니다 ...

package com.javadude.sample;

import java.util.HashMap;
import java.util.Map;

public class StrategyExample {
    static class SomeCommonSuperType {}
    static class SubType1 extends SomeCommonSuperType {}
    static class SubType2 extends SomeCommonSuperType {}
    static class SubType3 extends SomeCommonSuperType {}

    static interface Handler<T extends SomeCommonSuperType> {
        Object handle(T object);
    }

    static class HandlerMap {
        private Map<Class<? extends SomeCommonSuperType>, Handler<? extends SomeCommonSuperType>> handlers_ =
            new HashMap<Class<? extends SomeCommonSuperType>, Handler<? extends SomeCommonSuperType>>();
        public <T extends SomeCommonSuperType> void add(Class<T> c, Handler<T> handler) {
            handlers_.put(c, handler);
        }
        @SuppressWarnings("unchecked")
        public <T extends SomeCommonSuperType> Object handle(T o) {
            return ((Handler<T>) handlers_.get(o.getClass())).handle(o);
        }
    }

    public static void main(String[] args) {
        HandlerMap handlerMap = new HandlerMap();

        handlerMap.add(SubType1.class, new Handler<SubType1>() {
            @Override public Object handle(SubType1 object) {
                System.out.println("Handling SubType1");
                return null;
            } });
        handlerMap.add(SubType2.class, new Handler<SubType2>() {
            @Override public Object handle(SubType2 object) {
                System.out.println("Handling SubType2");
                return null;
            } });
        handlerMap.add(SubType3.class, new Handler<SubType3>() {
            @Override public Object handle(SubType3 object) {
                System.out.println("Handling SubType3");
                return null;
            } });

        SubType1 subType1 = new SubType1();
        handlerMap.handle(subType1);
        SubType2 subType2 = new SubType2();
        handlerMap.handle(subType2);
        SubType3 subType3 = new SubType3();
        handlerMap.handle(subType3);
    }
}



일반적으로 "instanceof"연산자가 (instanceof가이 기본 클래스의 하위 클래스를 확인하는) 그런 경우에 frowned되는 이유는 작업을 메서드로 옮기고 해당 작업을 재정의하는 것입니다. 서브 클래스. 예를 들어, 당신은 :

if (o instanceof Class1)
   doThis();
else if (o instanceof Class2)
   doThat();
//...

대체 할 수 있습니다.

o.doEverything();

Class1 호출에서 "doEverything ()"을 구현하고 doThis ()를 호출하고 Class2 호출에서 "doThat ()"을 호출하는 등의 작업을 수행 할 수 있습니다.




속도가 유일한 목표 인 경우 int 상수를 사용하여 하위 클래스를 식별하면 시간이 밀리 초 단위로 줄어들 것

static final int ID_A = 0;
static final int ID_B = 1;
abstract class Base {
  final int id;
  Base(int i) { id = i; }
}
class A extends Base {
 A() { super(ID_A); }
}
class B extends Base {
 B() { super(ID_B); }
}
...
Base obj = ...
switch(obj.id) {
case  ID_A: .... break;
case  ID_B: .... break;
}

끔찍한 OO 디자인,하지만 성능 분석이 병목 현상이 어디에 있는지 알려주는 경우. 내 코드에서 디스패치 코드는 총 실행 시간의 10 %를 차지하며 이는 전체 속도를 1 % 향상시키는 데 도움이 될 수 있습니다.




프로젝트에서 실제로 성능 문제 일 경우 / profile을 측정해야합니다. 가능한 경우 재 설계를 권장합니다. 나는 플랫폼의 네이티브 구현 (C로 작성)을 이길 수 없다고 확신한다. 이 경우 다중 상속도 고려해야합니다.

이 문제에 대해 더 자세히 알려야합니다. 예를 들어 구체적인 유형에만 관심이 있다면 연관 저장소 (예 : Map <Class, Object>)를 사용할 수 있습니다.




또한 enum 접근법을 선호하지만 하위 클래스가 getType() 메서드를 구현하도록 추상 기본 클래스를 사용합니다.

public abstract class Base
{
  protected enum TYPE
  {
    DERIVED_A, DERIVED_B
  }

  public abstract TYPE getType();

  class DerivedA extends Base
  {
    @Override
    public TYPE getType()
    {
      return TYPE.DERIVED_A;
    }
  }

  class DerivedB extends Base
  {
    @Override
    public TYPE getType()
    {
      return TYPE.DERIVED_B;
    }
  }
}



너는 틀린 것에 집중하고있어. instanceof와 같은 것을 검사하는 다른 방법의 차이는 아마도 측정 할 수 없을 것입니다. 성능이 중요한 경우 Java가 잘못된 언어 일 수 있습니다. 주된 이유는 VM이 ​​가비지 수집을 결정할 때 제어 할 수 없다는 것이므로 큰 프로그램에서 수 초 동안 CPU를 100 %로 사용할 수 있습니다 (MagicDraw 10은 그랬습니다). 이 프로그램이 실행될 모든 컴퓨터를 제어하지 않으면 JVM의 버전을 보장 할 수 없으며 이전 버전의 많은 JVM이 속도 문제를 가지고 있습니다. 작은 응용 프로그램이라면 Java로 괜찮을 지 모르지만 끊임없이 데이터를 읽고 폐기하면 GC가 시작될 때 알 수 있습니다.