Java でのinstanceofの使用によるパフォーマンスへの影響
-
01-07-2019 - |
質問
私はアプリケーションに取り組んでいますが、設計アプローチの 1 つは、 instanceof
オペレーター。OO の設計では通常、使用を避けようとすることはわかっていますが、 instanceof
, それは別の話であり、この質問は純粋にパフォーマンスに関連しています。パフォーマンスに影響があるかどうか疑問に思ったのですが?と同じくらい速いです ==
?
たとえば、10 個のサブクラスを持つ基本クラスがあるとします。基本クラスを受け取る 1 つの関数で、そのクラスがサブクラスのインスタンスであるかどうかをチェックし、何らかのルーチンを実行します。
これを解決するために私が考えた他の方法の 1 つは、代わりに「type id」整数プリミティブを使用し、ビットマスクを使用してサブクラスのカテゴリを表し、サブクラス「type id」とサブクラスのビットマスク比較を行うことでした。カテゴリを表す定数マスク。
は instanceof
どういうわけかJVMによってそれより高速になるように最適化されていますか?Java にこだわりたいのですが、アプリのパフォーマンスが重要です。以前にこの道をたどったことがある誰かがアドバイスを提供できれば素晴らしいでしょう。細かいことを気にしすぎているのでしょうか、それとも最適化すべき間違った点に焦点を当てているのでしょうか?
解決
最新の JVM/JIC コンパイラーは、instanceof、例外処理、リフレクションなど、従来の「遅い」操作のほとんどによるパフォーマンスへの影響を排除しています。
ドナルド・クヌースは次のように書いています、「我々は、97% の確率で、小さな効率のことは忘れるべきです。時期尚早の最適化が諸悪の根源です。」instanceof のパフォーマンスはおそらく問題ではないので、それが問題であると確信するまで、奇妙な回避策を考え出すことに時間を無駄にしないでください。
他のヒント
アプローチ
私が書いた ベンチマークプログラム さまざまな実装を評価するには:
instanceof
実装(参考として)- 抽象クラスを介したオブジェクト指向と
@Override
テスト方法 - 独自の型実装を使用する
getClass() == _.class
実装
私が使用した うーん 100 回のウォームアップ呼び出し、1000 回の測定反復、および 10 回のフォークでベンチマークを実行します。したがって、各オプションは 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 % |
先生
Java 1.8の場合 instanceof
が最も早いアプローチですが、 getClass()
とても近いです。
1 文字だけの文字列オブジェクトに対する単純な s.equals() 呼び出しと、instanceOf のパフォーマンスがどのように比較されるかを確認するための簡単なテストを作成しました。
10.000.000 ループでは、instanceOf では 63 ~ 96 ミリ秒、文字列等しい場合には 106 ~ 230 ミリ秒でした。
Java jvm 6を使用しました。
したがって、私の簡単なテストでは、1 つの文字列比較ではなく、instanceOf を実行する方が高速です。
文字列の代わりに整数の .equals() を使用しても同じ結果が得られましたが、== を使用した場合のみ、instanceOf より 20 ミリ秒高速でした (10.000.000 ループ内)
パフォーマンスへの影響を決定する項目は次のとおりです。
- instanceof 演算子が true を返す可能性のあるクラスの数
- データの分散 - ほとんどの instanceof 操作は 1 回目または 2 回目の試行で解決されますか?真の操作を返す可能性が最も高い操作を最初に置くとよいでしょう。
- 導入環境。Sun Solaris VM での実行は、Sun の Windows JVM とは大きく異なります。Solaris はデフォルトで「サーバー」モードで実行されますが、Windows はクライアント モードで実行されます。Solaris での JIT 最適化により、すべてのメソッドに同じようにアクセスできるようになります。
私が作成したのは、 4 つの異なる発送方法のマイクロベンチマーク. 。Solaris での結果は次のとおりです。数値が小さいほど高速になります。
InstanceOf 3156
class== 2925
OO 3083
Id 3067
最後の質問に答えると次のようになります。プロファイラから、instanceof に途方もない時間を費やしていると言われない限り、次のとおりです。はい、あなたはうるさいです。
最適化する必要のなかったものを最適化することについて考える前に、次のことを行ってください。アルゴリズムを最も読みやすい方法で記述して実行します。jit コンパイラー自体が最適化されるまで、それを実行します。このコード部分で問題が発生した場合は、プロファイラーを使用して、どこを最大限に活用して最適化すればよいかを示します。
コンパイラが高度に最適化されている場合、ボトルネックに関する推測は完全に間違っている可能性があります。
そして、この答えの真の精神に沿って(私は心から信じています):jit コンパイラーが最適化する機会を得た後、instanceof と == がどのように関係するのかはまったくわかりません。
忘れた:最初の実行では決して測定しないでください。
同じ質問がありましたが、私と同様のユースケースの「パフォーマンスメトリクス」が見つからなかったので、さらにサンプルコードを作成しました。私のハードウェアと Java 6 および 7 では、instanceof とスイッチオンの 10mln 反復の違いは次のとおりです。
for 10 child classes - instanceof: 1200ms vs switch: 470ms
for 5 child classes - instanceof: 375ms vs switch: 204ms
したがって、特に膨大な数の if-else-if ステートメントでは、instanceof は非常に遅くなりますが、実際のアプリケーションでは違いは無視できる程度です。
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
非常に高速で、ほんの数回の CPU 命令しか必要としません。
どうやら、クラスの場合は、 X
サブクラスがロードされていません (JVM は認識しています)。 instanceof
次のように最適化できます。
x instanceof X
==> x.getClass()==X.class
==> x.classID == constant_X_ID
主な費用は読むだけです!
もし X
サブクラスがロードされているため、さらにいくつかの読み取りが必要です。それらは同じ場所にある可能性が高いため、追加コストも非常に低くなります。
皆さん朗報です!
現実世界のほとんどの実装 (つまり、instanceof が本当に必要な実装) では、instanceof はおそらく単純な等価関数よりもコストが高くなるでしょう。すべての初心者向けの教科書や書籍などの一般的なメソッドをオーバーライドするだけでは解決できません。上記のデミアンが示唆しています)。
何故ですか?おそらく何が起こるかというと、いくつかの機能を提供するいくつかのインターフェイス (たとえば、インターフェイス x、y、z) と、それらのインターフェイスの 1 つを実装する (または実装しない) 操作対象のオブジェクトがあることになるからです。しかし直接ではありません。たとえば、次のようなものがあるとします。
w は x を拡張します
A は w を実装します
B は A を拡張します
C は B を拡張し、y を実装します
D は C を拡張し、z を実装します
D のインスタンス (オブジェクト d) を処理しているとします。(d instanceof x) を計算するには、d.getClass() を取得し、実装されているインターフェイスをループして、x に対して == であるかどうかを確認する必要があります。そうでない場合は、すべての祖先に対して再帰的にこれを実行します。私たちの場合、そのツリーの幅優先探索を行うと、y と z が何も拡張しないと仮定すると、少なくとも 8 つの比較が生成されます。
実際の導出ツリーの複雑さは、より複雑になる可能性があります。場合によっては、すべての可能なケースにおいて、d を x を拡張するもののインスタンスとして事前に解決できれば、JIT はそのほとんどを最適化できます。ただし、現実的には、ほとんどの場合、そのツリートラバースを通過することになります。
それが問題になる場合は、代わりにハンドラー マップを使用し、オブジェクトの具象クラスを処理を行うクロージャにリンクすることをお勧めします。これにより、ツリー トラバーサル フェーズが削除され、直接マッピングが優先されます。ただし、C.class のハンドラーを設定した場合、上記のオブジェクト d は認識されないことに注意してください。
これが私の 2 セントです、お役に立てば幸いです...
「instanceof」は実際には + や - のような演算子であり、独自の JVM バイトコード命令があると思います。十分に速いはずです。
オブジェクトがサブクラスのインスタンスであるかどうかをテストするスイッチがある場合、設計をやり直す必要があるかもしれないということは避けるべきです。サブクラス固有の動作をサブクラス自体にプッシュすることを検討してください。
インスタンスオブは非常に高速です。つまり、クラス参照の比較に使用されるバイトコードになります。ループ内で数百万のinstanceofを試して、自分の目で確認してください。
デミアンとポールは良い点について言及しています。 しかし, 、実行するコードの配置は、データをどのように使用したいかによって異なります。
私は、さまざまな方法で使用できる小さなデータ オブジェクトの大ファンです。オーバーライド (ポリモーフィック) アプローチに従う場合、オブジェクトは「一方向」でのみ使用できます。
ここでパターンが登場します...
ダブルディスパッチ (訪問者パターンの場合と同様) を使用して、各オブジェクトに自分自身を渡して「電話をかける」ように依頼できます。これにより、オブジェクトのタイプが解決されます。 しかし (繰り返しになりますが) 考えられるすべてのサブタイプを「実行」できるクラスが必要になります。
私は、処理したいサブタイプごとに戦略を登録できる戦略パターンを使用することを好みます。以下のようなものです。これは型の正確な一致にのみ役立ちますが、拡張可能であるという利点があることに注意してください。サードパーティの貢献者は独自の型とハンドラーを追加できます。(これは、新しいバンドルを追加できる 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 を大量に使用すると、設計上の問題が発生する可能性があります。
xClass == String.class を使用できる場合は、この方が高速です。注記:最終クラスにはinstanceofは必要ありません。
特定の JVM がインスタンスをどのように実装するかを言うのは難しいですが、ほとんどの場合、オブジェクトは構造体と同等であり、クラスも同様であり、すべてのオブジェクト構造体は、インスタンスであるクラス構造体へのポインターを持っています。実際には、instanceof
if (o instanceof java.lang.String)
次の C コードと同じくらい高速になる可能性があります
if (objectStruct->iAmInstanceOf == &java_lang_String_class)
JIT コンパイラが導入されており、適切な機能を実行すると仮定します。
これがポインタにアクセスし、そのポインタが指す特定のオフセットでポインタを取得し、これを別のポインタと比較するだけであることを考えると(これは基本的に 32 ビット数値が等しいかどうかをテストするのと同じです)、この操作は実際にはできると思いますとても早くしてください。
ただし、そうする必要はありません。JVM に大きく依存します。ただし、これがコード内のボトルネック操作であることが判明する場合は、JVM 実装がかなり貧弱であると考えられます。JIT コンパイラを持たず、コードを解釈するだけの人でも、実質的に時間をかけずに instanceof テストを作成できるはずです。
インスタンスの これは、オブジェクト指向設計が不十分であるという警告です。
現在の JVM は、 インスタンスの それ自体はパフォーマンス上の問題ではありません。特にコア機能でそれを頻繁に使用していることに気付いたら、おそらくデザインを検討する時期が来ているでしょう。より良い設計へのリファクタリングによるパフォーマンス (およびシンプルさ/保守性) の向上は、実際のプロセッサ サイクルにかかる時間を大幅に上回ります。 インスタンスの 電話。
非常に小さな単純化したプログラミングの例を示します。
if (SomeObject instanceOf Integer) {
[do something]
}
if (SomeObject instanceOf Double) {
[do something different]
}
アーキテクチャが貧弱であれば、SomeObject を 2 つの子クラスの親クラスにし、各子クラスがメソッド (doSomething) をオーバーライドすることで、コードは次のようになります。
Someobject.doSomething();
インスタンスのパフォーマンスについては折り返しご連絡いたします。しかし、問題 (または問題の欠如) を完全に回避する方法は、instanceof を実行する必要があるすべてのサブクラスへの親インターフェイスを作成することです。インターフェイスは次のスーパーセットになります。 全て インスタンスのチェックを行う必要があるサブクラスのメソッド。メソッドが特定のサブクラスに適用されない場合は、このメソッドのダミー実装を提供するだけです。この問題を誤解していなければ、これが私が過去にこの問題を回避した方法です。
一般に、このような場合 (instanceof がこの基本クラスのサブクラスをチェックしている場合) に「instanceof」演算子が嫌われる理由は、操作をメソッドに移動し、適切なメソッドでそれをオーバーライドすることであるためです。サブクラス。たとえば、次のような場合です。
if (o instanceof Class1)
doThis();
else if (o instanceof Class2)
doThat();
//...
それを次のように置き換えることができます
o.doEverything();
次に、Class1 の「doEverything()」の実装で「doThis()」を呼び出し、Class2 で「doThat()」を呼び出す、というようになります。
最新の Java バージョンでは、instanceof 演算子は単純なメソッド呼び出しとして高速です。これはつまり:
if(a instanceof AnyObject){
}
は次のように高速になります。
if(a.getType() == XYZ){
}
もう1つは、多数のinstanceofをカスケードする必要がある場合です。その場合、getType() を 1 回だけ呼び出すスイッチの方が高速です。
速度だけが目的の場合は、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% の速度向上に貢献している可能性があります。
jmh-java-benchmark-archetype:2.21 に基づいてパフォーマンス テストを作成します。JDK は openjdk で、バージョンは 1.8.0_212 です。テストマシンはMac Proです。テスト結果は次のとおりです。
Benchmark Mode Cnt Score Error Units
MyBenchmark.getClasses thrpt 30 510.818 ± 4.190 ops/us
MyBenchmark.instanceOf thrpt 30 503.826 ± 5.546 ops/us
結果は次のことを示しています。getClass は、他のテストとは逆に、instanceOf よりも優れています。しかし、その理由はわかりません。
テストコードは以下のとおりです。
public class MyBenchmark {
public static final Object a = new LinkedHashMap<String, String>();
@Benchmark
@BenchmarkMode(Mode.Throughput)
@OutputTimeUnit(TimeUnit.MICROSECONDS)
public boolean instanceOf() {
return a instanceof Map;
}
@Benchmark
@BenchmarkMode(Mode.Throughput)
@OutputTimeUnit(TimeUnit.MICROSECONDS)
public boolean getClasses() {
return a.getClass() == HashMap.class;
}
public static void main(String[] args) throws RunnerException {
Options opt =
new OptionsBuilder().include(MyBenchmark.class.getSimpleName()).warmupIterations(20).measurementIterations(30).forks(1).build();
new Runner(opt).run();
}
}
それが実際にプロジェクトのパフォーマンスの問題であるかどうかを測定/プロファイルする必要があります。その場合は、可能であれば再設計をお勧めします。このプラットフォームのネイティブ実装 (C で書かれた) に勝るものはないと確信しています。この場合、多重継承についても考慮する必要があります。
問題についてもっと詳しく伝える必要があります。おそらく連想ストアを使用することもできます。具体的な型のみに興味がある場合は、 Map<Class, Object>。
Peter Lawrey の、最終クラスには instanceof は必要なく、参照の等価性を使用するだけでよいという注記に関しては、注意してください。最終クラスは拡張できませんが、同じクラスローダーによってロードされることは保証されません。x.getClass() == SomeFinal.class またはその類似品は、コードのそのセクションで使用されているクラスローダーが 1 つだけであることが絶対的に確実な場合にのみ使用してください。
私は 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」は心配するほど高価ではないというこのページの一般的なコンセンサスに対する反例を提出する価値があるかもしれないと考えました。(最適化の歴史的な試みで) 内部ループにコードがあることがわかりました。
if (!(seq instanceof SingleItem)) {
seq = seq.head();
}
ここで、SingleItem で head() を呼び出すと、値が変更されずに返されます。コードを置き換えると、
seq = seq.head();
ループ内で文字列から倍精度への変換など、かなり重い処理が行われているにもかかわらず、速度が 269 ミリ秒から 169 ミリ秒に向上しました。もちろん、instanceof 演算子自体を削除したことよりも、条件分岐を削除したことによる高速化の可能性が高くなります。しかし、言及する価値があると思いました。
あなたは間違ったことに焦点を当てています。同じことをチェックするためのinstanceofと他のメソッドとの違いは、おそらく測定すらできないでしょう。パフォーマンスが重要である場合、Java はおそらく間違った言語です。主な理由は、VM がガベージの収集を開始するタイミングを制御できないため、大規模なプログラムでは数秒間 CPU が 100% になる可能性があります (MagicDraw 10 はその点で優れていました)。このプログラムが実行されるすべてのコンピュータを制御していない限り、どのバージョンの JVM で動作するかは保証できません。また、古いバージョンの多くには重大な速度の問題がありました。小さなアプリであれば Java で問題ないかもしれませんが、常にデータを読み取ったり破棄したりする場合は、 意思 GC が開始されるときに注目してください。