Javaパフォーマンスプログラミング、パート2:キャストのコスト

Javaパフォーマンスに関するシリーズのこの2番目の記事では、キャストに焦点が移ります。キャストとは何か、コストはどれくらいか、そして(場合によっては)それを回避する方法です。今月は、クラス、オブジェクト、および参照の基本を簡単に確認してから、ハードコアパフォーマンスの数値(きしみを不快にしないようにサイドバーで!)とガイドラインを確認します。 Java仮想マシン(JVM)を消化不良にする可能性が最も高い操作のタイプ。最後に、キャストを引き起こす可能性のある一般的なクラス構造化効果を回避する方法について詳しく見ていきます。

Javaパフォーマンスプログラミング:シリーズ全体を読んでください!

  • パート1。オブジェクトの作成とガベージコレクションを制御することにより、プログラムのオーバーヘッドを削減し、パフォーマンスを向上させる方法を学びます。
  • パート2。タイプセーフコードによるオーバーヘッドと実行エラーの削減
  • パート3.コレクション代替案のパフォーマンスがどのように測定されるかを確認し、各タイプを最大限に活用する方法を見つけます

Javaのオブジェクトと参照型

先月、Javaのプリミティブ型とオブジェクトの基本的な違いについて説明しました。プリミティブ型の数とそれらの間の関係(特に型間の変換)は、言語定義によって固定されています。一方、オブジェクトは無制限のタイプであり、他の任意の数のタイプに関連している可能性があります。

Javaプログラムの各クラス定義は、新しいタイプのオブジェクトを定義します。これにはJavaライブラリのすべてのクラスが含まれるため、特定のプログラムが数百または数千もの異なるタイプのオブジェクトを使用している可能性があります。これらのタイプのいくつかは、特定の特別な用途を有するか、または(例えばの使用として扱うようにJava言語定義によって指定されているjava.lang.StringBufferためjava.lang.String、連結操作)。ただし、これらのいくつかの例外を除いて、すべてのタイプは、Javaコンパイラとプログラムの実行に使用されるJVMによって基本的に同じように扱われます。

クラス定義が(extendsクラス定義ヘッダーの節によって)別のクラスを親またはスーパークラスとして指定しない場合、それは暗黙的にjava.lang.Objectクラスを拡張します。これは、すべてのクラスがjava.lang.Object、直接または1つ以上のレベルの親クラスのシーケンスを介して最終的に拡張されることを意味します。

オブジェクト自体は常にクラスのインスタンスであり、オブジェクトのタイプはそれがインスタンスであるクラスです。ただし、Javaでは、オブジェクトを直接処理することはありません。オブジェクトへの参照を処理します。たとえば、次の行です。

 java.awt.Component myComponent; 

java.awt.Componentオブジェクトを作成しません。タイプの参照変数を作成しますjava.lang.Component。参照にはnullオブジェクトと同じタイプがありますが、参照タイプとオブジェクトタイプが正確に一致するわけではありません。参照値は、参照と同じタイプのオブジェクト、または任意のサブクラスのオブジェクト(つまり、クラスの子孫)である可能性があります。 from)参照のタイプ。この特定のケースでjava.awt.Componentは、は抽象クラスであるため、参照と同じ型のオブジェクトは存在しないことがわかっていますが、その参照型のサブクラスのオブジェクトは確かに存在する可能性があります。

ポリモーフィズムとキャスティング

参照のタイプによって、参照されるオブジェクト(つまり、参照の値であるオブジェクト)をどのように使用できるかが決まります。たとえば、上記の例では、を使用myComponentするコードjava.awt.Componentは、参照されるオブジェクトのクラスまたはそのスーパークラスによって定義されたメソッドのいずれかを呼び出すことができます。

ただし、呼び出しによって実際に実行されるメソッドは、参照自体のタイプではなく、参照されるオブジェクトのタイプによって決定されます。これがポリモーフィズムの基本原則です。サブクラスは、異なる動作を実装するために、親クラスで定義されたメソッドをオーバーライドできます。変数の例の場合、参照されるオブジェクトが実際にのインスタンスである場合java.awt.ButtonsetLabel("Push Me")呼び出しの結果として生じる状態の変化は、参照されるオブジェクトがのインスタンスである場合の結果とは異なりますjava.awt.Label

クラス定義に加えて、Javaプログラムもインターフェース定義を使用します。インターフェイスとクラスの違いは、インターフェイスは一連の動作(場合によっては定数)のみを指定するのに対し、クラスは実装を定義することです。インターフェイスは実装を定義しないため、オブジェクトをインターフェイスのインスタンスにすることはできません。ただし、インターフェイスを実装するクラスのインスタンスにすることもできます。参照はインターフェイスタイプにすることができます。その場合、参照されるオブジェクトは、インターフェイスを実装する任意のクラスのインスタンスである可能性があります(直接またはいくつかの祖先クラスを介して)。

キャストは、タイプ間、特に参照タイプ間での変換に使用されます。ここで関心のあるキャスト操作のタイプについてはそうです。アップキャスト操作(Java言語仕様では拡張変換とも呼ばれます)は、サブクラス参照を祖先クラス参照に変換します。このキャスト操作は、常に安全であり、コンパイラーによって直接実装できるため、通常は自動です。

ダウンキャスト操作(Java言語仕様ではローイング変換とも呼ばれます)は、祖先クラス参照をサブクラス参照に変換します。 Javaでは、キャストが有効であることを確認するために実行時にキャストをチェックする必要があるため、このキャスト操作は実行オーバーヘッドを作成します。参照されるオブジェクトがキャストのターゲットタイプまたはそのタイプのサブクラスのインスタンスではない場合、試行されたキャストは許可されないため、をスローする必要がありjava.lang.ClassCastExceptionます。

instanceofJavaの演算子を使用すると、実際に操作を試行せずに、特定のキャスト操作が許可されているかどうかを判断できます。チェックのパフォーマンスコストは、許可されていないキャスト試行によって生成された例外のパフォーマンスコストよりもはるかに低いためinstanceof、参照のタイプが希望どおりであるかどうかわからない場合は、通常、テストを使用することをお勧めします。 。ただし、そうする前に、不要な型の参照を処理する合理的な方法があることを確認する必要があります。そうでない場合は、例外をスローして、コードのより高いレベルで処理することもできます。

風に注意を払う

キャストにより、Javaでジェネリックプログラミングを使用できるようになります。この場合、コードは、ある基本クラス(多くの場合java.lang.Object、ユーティリティクラス)の子孫であるクラスのすべてのオブジェクトで機能するように記述されます。ただし、鋳造を使用すると、固有の問題が発生します。次のセクションでは、パフォーマンスへの影響を見ていきますが、最初にコード自体への影響を考えてみましょう。汎用java.lang.Vectorコレクションクラスを使用したサンプルを次に示します。

プライベートベクターsomeNumbers; ... public void doSomething(){... int n = ...整数値=(整数)someNumbers.elementAt(n); ...}

このコードは、明確さと保守性の観点から潜在的な問題を提示します。元の開発者以外の誰かが、ある時点でコードを変更した場合、これはのサブクラスであるjava.lang.Doubleため、someNumbersコレクションにを追加できると合理的に考えるかもしれませんjava.lang.Number。これを試してみるとすべてが正常にコンパイルされますが、実行の不確定な時点で、java.lang.ClassCastExceptionへのキャストの試行java.lang.Integerが付加価値のために実行されたときにスローされる可能性があります。

ここでの問題は、キャストを使用すると、Javaコンパイラに組み込まれている安全性チェックがバイパスされることです。コンパイラーはエラーをキャッチしないため、プログラマーは実行中にエラーを探すことになります。これ自体は悲惨なことではありませんが、このタイプの使用エラーは、コードのテスト中に非常に巧妙に隠れて、プログラムが本番環境に移行したときに明らかになることがよくあります。

当然のことながら、コンパイラーがこのタイプの使用エラーを検出できるようにする手法のサポートは、Javaに対してより強く要求されている拡張機能の1つです。このサポートの追加を調査しているJavaCommunity Processで現在進行中のプロジェクトがあります:プロジェクト番号JSR-000014、Javaプログラミング言語へのジェネリック型の追加(詳細については、以下の「リソース」セクションを参照してください)。この記事の続きでは、来月、私たちはこのプロジェクトをより詳細に見て、それがどのように役立つ可能性があり、どこで私たちがもっと欲しくなる可能性があるかについて議論します。

パフォーマンスの問題

キャストはJavaのパフォーマンスに悪影響を与える可能性があり、頻繁に使用されるコードでのキャストを最小限に抑えることでパフォーマンスを向上できることが長い間認識されてきました。メソッド呼び出し、特にインターフェースを介した呼び出しも、潜在的なパフォーマンスのボトルネックとしてよく言及されます。ただし、現世代のJVMは前世代から大きく進歩しており、これらの原則が今日どの程度維持されているかを確認する価値があります。

この記事では、これらの要素が現在のJVMのパフォーマンスにとってどれほど重要であるかを確認するための一連のテストを開発しました。テスト結果は、サイドバーの2つの表にまとめられています。表1はメソッド呼び出しのオーバーヘッドを示し、表2はキャストのオーバーヘッドを示しています。テストプログラムの完全なソースコードはオンラインでも入手できます(詳細については、以下の「リソース」セクションを参照してください)。

表の詳細をざっと読みたくない読者のためにこれらの結論を要約すると、特定のタイプのメソッド呼び出しとキャストは依然としてかなり高価であり、場合によっては単純なオブジェクト割り当てとほぼ同じくらいの時間がかかります。可能な場合、パフォーマンスを最適化する必要があるコードでは、これらのタイプの操作を回避する必要があります。

特に、オーバーライドされたメソッド(オブジェクトの実際のクラスだけでなく、ロードされたクラスでオーバーライドされるメソッド)の呼び出しやインターフェイスを介した呼び出しは、単純なメソッド呼び出しよりもかなりコストがかかります。テストで使用されたHotSpotServer JVM 2.0ベータは、多くの単純なメソッド呼び出しをインラインコードに変換し、そのような操作のオーバーヘッドを回避します。ただし、HotSpotは、オーバーライドされたメソッドとインターフェイスを介した呼び出しについて、テストされたJVMの中で最悪のパフォーマンスを示しています。

キャスト(もちろんダウンキャスト)の場合、テストされたJVMは通常、パフォーマンスヒットを妥当なレベルに保ちます。HotSpotは、ほとんどのベンチマークテストでこれを使用して例外的な仕事をし、メソッド呼び出しと同様に、多くの単純なケースでキャストのオーバーヘッドをほぼ完全に排除できます。キャストの後にオーバーライドされたメソッドの呼び出しが続くなど、より複雑な状況では、テストされたすべてのJVMで顕著なパフォーマンスの低下が見られます。

テストされたバージョンのHotSpotは、オブジェクトが(常に同じターゲットタイプにキャストされるのではなく)異なる参照タイプに連続してキャストされた場合にも、非常に低いパフォーマンスを示しました。この状況は、クラスの深い階層を使用するSwingなどのライブラリで定期的に発生します。

ほとんどの場合、先月の記事で見たオブジェクト割り当て時間と比較して、メソッド呼び出しとキャストの両方のオーバーヘッドは小さいです。ただし、これらの操作はオブジェクトの割り当てよりもはるかに頻繁に使用されることが多いため、パフォーマンスの問題の重大な原因となる可能性があります。

この記事の残りの部分では、コードでキャストする必要性を減らすためのいくつかの特定の手法について説明します。具体的には、サブクラスが基本クラスと相互作用する方法からキャストがどのように発生するかを調べ、このタイプのキャストを排除するためのいくつかの手法を探ります。来月、このキャストの第2部では、キャストのもう1つの一般的な原因である、ジェネリックコレクションの使用について検討します。

基本クラスとキャスト

Javaプログラムでのキャストの一般的な使用法はいくつかあります。たとえば、キャストは、多くの場合、いくつかのサブクラスによって拡張される可能性のある基本クラスの一部の機能の一般的な処理に使用されます。次のコードは、この使用法のやや不自然な図を示しています。

//サブクラスを持つ単純な基本クラスpublicabstract class BaseWidget {...} public class SubWidget extends BaseWidget {... public void doSubWidgetSomething(){...}} ... //以前のセットを使用して、サブクラスを持つ基本クラスクラスのパブリック抽象クラスBaseGorph {//このGorphに関連付けられたウィジェットprivateBaseWidget myWidget; ... //このGorphに関連付けられたウィジェットを設定します(サブクラスでのみ許可されます)protected void setWidget(BaseWidget widget){myWidget = widget; } //このGorphに関連付けられたウィジェットを取得しますpublicBaseWidget getWidget(){return myWidget; } ... //このGorphと何らかの関係があるGorphを返す//これは常に呼び出されるのと同じ型になりますが、//基本クラスのインスタンスのみを返すことができますpublic abstract BaseGorph otherGorph(){。 ..}} //ウィジェットサブクラスを使用するGorphサブクラスpublicclass SubGorph extends BaseGorph {//このGorphと何らかの関係を持つGorphを返すpublicBaseGorph otherGorph(){...} ... public void anyMethod(){... / /使用しているウィジェットを設定しますSubWidgetwidget = ... setWidget(widget); ... //ウィジェットを使用する((SubWidget)getWidget())。doSubWidgetSomething(); ... // otherGorphを使用SubGorphother =(SubGorph)otherGorph(); ...}}