Javaでinstanceofを使用した場合のパフォーマンスへの影響


Answers

アプローチ

私はさまざまな実装を評価するベンチマークプログラムを書い

  1. instanceof実装(参照として)
  2. 抽象クラスを介してオブジェクトを指向させ、 @Overrideテストメソッドを@Overrideする
  3. 独自の型実装を使用する
  4. getClass() == _.class実装

私は100回のウォームアップコール、測定中の1000回の反復、10回のフォークでベンチマークを実行するためにjmhを使用しjmhた。 各オプションは10,000回測定され、MacOS 10.12.4とJava 1.8でMacBook Pro全体のベンチマークを実行するために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

私はアプリケーションに取り組んでおり、1つの設計アプローチではinstanceof演算子の使用量が非常に多くなりinstanceof 。 オブジェクト指向設計は一般的にinstanceof使用を避けようとしていinstanceof 、これは別の話です。この質問は純粋にパフォーマンスに関連しています。 パフォーマンスに影響があるかどうか疑問に思っていましたか? それは==と同じくらい速いですか?

たとえば、10のサブクラスを持つ基本クラスがあります。 基本クラスを取る単一の関数では、クラスがサブクラスのインスタンスであるかどうかをチェックし、ルーチンを実行します。

私がそれを解決するために考えた他の方法の1つは、代わりに "type id"整数プリミティブを使用して、ビットマスクを使用してサブクラスのカテゴリを表し、次にサブクラス "type id"のビットマスク比較をカテゴリを表す定数マスク。

instanceofは、それよりも速くなるようにJVMによって何とか最適化されていますか? 私はJavaに固執したいが、アプリケーションのパフォーマンスは非常に重要です。 この道を歩いていた誰かがアドバイスをすることができれば涼しいでしょう。 私はあまりにも多くのニックピッキングをしているのか、間違ったことに集中して最適化していますか?




デミアンとポールは良い点を述べています。 しかし 、実際に実行するコードの配置は、データの使い方に依存します...

私は、さまざまな方法で使用できる小さなデータオブジェクトの大ファンです。 オーバーライド(ポリモフィック)アプローチに従うと、オブジェクトは「一方向」でしか使用できません。

これはパターンが入る場所です...

ダブルディスパッチ(ビジターパターンのように)を使用して、各オブジェクトにそれ自身を渡すように指示することができます。これにより、オブジェクトのタイプが解決されます。 しかし、 (再び)あなたは、可能なすべてのサブタイプを「詰める」ことができるクラスが必要です。

私は戦略パターンを使用することをお勧めします。ここでは、扱う各サブタイプの戦略を登録できます。 次のようなもの これは厳密な型の一致にのみ役立ちますが、拡張可能であるという利点があります。サードパーティのコントリビュータは独自の型とハンドラを追加できます。 (これは、新しいバンドルを追加できる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"が気になるほど高価ではないということです。 私は内部ループにいくつかのコードがあることを発見しました。

if (!(seq instanceof SingleItem)) {
  seq = seq.head();
}

ここでは、SingleItemのhead()を呼び出しても値は変更されません。 コードを

seq = seq.head();

ストリングからダブルへの変換のような、ループ内でかなり重いことが起こっているにもかかわらず、269msから169msへのスピードアップが可能です。 もちろん、instanceof演算子自体を削除するよりも、条件分岐を削除するほうがスピードアップになる可能性があります。 私はそれが言及する価値があると思った。




私は同じ質問がありますが、私に似たユースケースのための 'パフォーマンスメトリック'が見つからなかったので、私はいくつかのサンプルコードを実行しました。 私のハードウェアとJava 6&7では、10millionの反復でinstanceofとswitchの違いは

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'は実際には+や - のような演算子であり、独自のJVMバイトコード命令を持っていると思います。 それは十分に速くなければなりません。

オブジェクトがいくつかのサブクラスのインスタンスであるかどうかをテストしているスイッチがある場合は、デザインを再作成する必要があるかもしれません。 サブクラス固有の動作をサブクラス自体にプッシュすることを検討してください。




現代のJavaバージョンでは、単純なメソッド呼び出しのようにinstanceof演算子が高速です。 これの意味は:

if(a instanceof AnyObject){
}

次のように高速です。

if(a.getType() == XYZ){
}

別のことは、多くのインスタンスをカスケードする必要がある場合です。 一度getType()を呼び出すだけの方が高速です。




私はあなたのパフォーマンスのインスタンスであなたに戻ってきます。 しかし、問題(またはその欠如)を避ける方法は、instanceofを実行する必要のあるすべてのサブクラスに対する親インタフェースを作成することです。 インタフェースは、インスタンスのチェックを行う必要があるサブクラスのすべてのメソッドのスーパーセットになります。 メソッドが特定のサブクラスに適用されない場合は、このメソッドのダミー実装を提供するだけです。 私がこの問題を誤解しなかった場合、これは私が過去の問題をどのように克服したかです。




ピーター・ローリーのメモでは、最終クラスにはinstanceofは必要なく、参照平等を使うことができますので注意してください。 最終的なクラスは拡張できませんが、同じクラスローダーによってロードされることは保証されません。 x.getClass()== SomeFinal.classまたはそのilkを使用するのは、そのコードセクションで演奏中のクラスローダーが1つだけであることが絶対的な場合です。




特定のJVMがどのようにインスタンスを実装しているのかを言うのは難しいですが、ほとんどの場合、オブジェクトは構造体に匹敵し、クラスも同様であり、すべてのオブジェクト構造体はインスタンスであるクラス構造体へのポインタを持っています。 実際には

if (o instanceof java.lang.String)

次のCコードと同じくらい速いかもしれません

if (objectStruct->iAmInstanceOf == &java_lang_String_class)

JITコンパイラが適切な場所にあり、まともな仕事をしていると仮定します。

これがポインタにのみアクセスしていることを考慮すると、ポインタがあるオフセットでポインタを指し示し、これを別のポインタと比較すると(これは基本的に同じ32ビットの数値にテストするのと同じです)、実際には非常に速い。

しかし、それはJVMに大きく依存している必要はありません。 しかし、これがコード内のボトルネック操作であることが判明した場合は、JVMの実装はやや難しいと考えています。 JITコンパイラを持たず、コードを解釈するだけでも、事実上時間のないインスタンステストを行うことができます。




instanceofはたいていの現実世界の実装(すなわち、instanceofが本当に必要とされる単純なequalsよりもコストがかかるでしょう。そして、あなたは、初心者の教科書のように共通のメソッドをオーバーライドするだけでは解決できません。上記のデミアン)。

何故ですか? たぶん起こるのは、いくつかのインターフェース(いくつかのインターフェースx、y、zと言う)といくつかのオブジェクトを扱ういくつかのインターフェースを持っていることです。直接ではありません。 例えば、私が持っていることを言う:

wはxを延長する

Aの実装

BはAを伸ばす

CはBを拡張し、yを実装します。

DはCを拡張し、zを実装します

オブジェクトDのインスタンスDを処理しているとします。 計算(d instanceof x)は、d.getClass()をとり、それが実装するインタフェースをループして、==〜xであるかどうかを調べる必要があります。そうでなければ、すべての祖先に対して再帰的に繰り返します。あなたがその木の最初の探査を幅広く行うならば、yとzが何も延長しないと仮定すると、少なくとも8回の比較が得られます...

実際の導出ツリーの複雑さはより高くなる可能性があります。 場合によっては、可能であれば、xを拡張するもののインスタンスをdとして事前に解決することができれば、JITはそのほとんどを最適化できます。 しかし、現実的には、あなたはその木をほとんど通り抜けようとしています。

それが問題になる場合は、代わりにハンドラマップを使用して、オブジェクトの具象クラスをハンドリングを行うクロージャにリンクすることをお勧めします。 これは、ツリー・トラバーサル・フェーズを除去し、ダイレクト・マッピングを優先します。 ただし、C.classのハンドラを設定していれば、上記のオブジェクトdは認識されないことに注意してください。

ここに私の2セントです、私は彼らが助けて欲しい...




パフォーマンスへの影響を決定する項目は次のとおりです。

  1. instanceof演算子がtrueを返す可能性のあるクラスの数
  2. あなたのデータの配布 - 最初の試みか2番目の試みで解決されたオペレーションの大半ですか? 真のオペレーションを最初に返す可能性が最も高いと考えられます。
  3. デプロイメント環境。 Sun Solaris VMでの実行は、SunのWindows JVMとは大きく異なります。 Solarisはデフォルトで「サーバー」モードで実行され、Windowsはクライアントモードで実行されます。 Solaris上のJIT最適化により、すべてのメソッドへのアクセスが同じになります。

私は、 4つの異なる発送方法のためマイクロベンチマークを作成しました。 Solarisの結果は以下のようになりますが、数字が小さいほど高速です。

InstanceOf 3156
class== 2925 
OO 3083 
Id 3067