ソフトウェア開発において、プログラミング言語Javaは、歴史的にCやC++などの最速の第3世代型付き言語よりも遅いと考えられていました。[ 1 ]これらの言語とは対照的に、Javaはデフォルトで、実際のコンピュータハードウェアとは異なる操作を行うJava仮想マシン(JVM)にコンパイルされます。初期のJVM実装はインタプリタであり、仮想操作をハードウェアで直接実行するためのマシンコードに変換するのではなく、仮想操作を1つずつシミュレートしていました。
1990年代後半以降、Java プログラムの実行速度は、ジャストインタイムコンパイル(JIT) の導入 (1997 年にJava 1.1で導入)、[ 2 ] [ 3 ] [ 4 ]、より優れたコード分析をサポートする言語機能の追加、および JVM の最適化 ( 2000 年にHotSpot がSunの JVMのデフォルトになったなど) により大幅に向上しました。高度なガベージコレクション戦略も改善された分野です。ARM のJazelleが提供するような Java バイトコードのハードウェア実行は検討されましたが、実用化には至りませんでした。
JavaバイトコードでコンパイルされたJavaプログラムのパフォーマンスは、ホストJava仮想マシン(JVM)がタスクをどれだけ最適に管理しているか、そしてJVMがコンピュータのハードウェアとオペレーティングシステム(OS)の機能をどれだけ効果的に活用しているかに依存します。そのため、Javaのパフォーマンステストや比較を行う際には、必ず使用したJVMのバージョン、ベンダー、OS、ハードウェアアーキテクチャを報告する必要があります。同様に、ネイティブコンパイルされた同等のプログラムのパフォーマンスは、生成されたマシンコードの品質に依存するため、テストや比較では、使用したコンパイラの名前、バージョン、ベンダー、および有効になっているコンパイラ最適化ディレクティブも報告する必要があります。
数々の最適化によって、JVMのパフォーマンスは時間とともに向上してきた。Javaはこれらの最適化を最初に成功裏に実装した仮想マシンであることが多いが、他の類似プラットフォームでも広く利用されている。
初期の JVM は常にJava バイトコードを解釈していました。そのため、平均的なアプリケーションでは Java は C に比べて 10 ~ 20 倍ものパフォーマンス低下を招いていました。[ 5 ]この問題を解決するために、Java 1.1 でジャストインタイム (JIT) コンパイラが導入されました。コンパイルのコストが高かったため、 Java 1.2 でHotSpotと呼ばれる追加システムが導入され、Java 1.3 でデフォルトになりました。このフレームワークを使用すると、Java 仮想マシンは、頻繁にまたは繰り返し実行されるホットスポットについてプログラムのパフォーマンスを継続的に分析します。これらのホットスポットは最適化の対象となり、パフォーマンスがそれほど重要でないコードでは最小限のオーバーヘッドで高性能な実行を実現します。 [ 6 ] [ 7 ] 一部のベンチマークでは、この方法で 10 倍の速度向上が示されています。[ 8 ]ただし、時間の制約により、コンパイラはプログラムを完全に最適化することができず、結果として得られるプログラムはネイティブ コードの代替案よりも遅くなります。[ 9 ] [ 10 ]
適応型最適化とは、コンピュータサイエンスにおける手法の一つで、現在の実行プロファイルに基づいてプログラムの一部を動的に再コンパイルするものです。単純な実装では、適応型オプティマイザは、ジャストインタイムコンパイルと命令の解釈の間でトレードオフを行うだけかもしれません。さらに高度なレベルでは、適応型最適化はローカルなデータ条件を利用して分岐を削除し、インライン展開を利用することもあります。
HotSpotのようなJava仮想マシンは、以前はJITで最適化されていたコードを非最適化することもできます。これにより、積極的な(そして潜在的に危険な)最適化を実行しながら、後でコードを非最適化して安全なパスに戻すことができます。[ 11 ] [ 12 ]
Java 1.0 および 1.1 のJava 仮想マシン(JVM) はマーク スイープ コレクタを使用しており、ガベージ コレクション後にヒープが断片化される可能性がありました。Java 1.2 以降、JVM は世代コレクタに変更され、断片化の解消動作が大幅に改善されました。[ 13 ]最新の JVM は、ガベージ コレクションのパフォーマンス をさらに向上させるさまざまな方法を使用しています。[ 14 ]
圧縮オブジェクト ポインタにより、Java 5.0 以降では 32 ビット参照で最大 32 GB のヒープをアドレス指定できます。Java は個々のバイトへのアクセスをサポートしておらず、デフォルトでは 8 バイト境界にアラインされたオブジェクトのみをサポートしています。このため、ヒープ参照の最下位 3 ビットは常に 0 になります。32 ビット参照の解像度を 8 バイト ブロックに下げることで、アドレス指定可能な領域を 32 GB まで増やすことができます。Java は C++ などの一部の言語よりも参照をはるかに多く使用するため、64 ビット参照を使用する場合と比較して、メモリ使用量が大幅に削減されます。Java 8 では、16 バイト境界などのより大きなアラインメントをサポートし、32 ビット参照で最大 64 GB まで対応できます。
Sun JVMは、クラスを実行する前に、そのJavaバイトコードを検証します(バイトコード検証ツールを参照)。この検証は遅延実行されます。つまり、クラスのバイトコードは、特定のクラスがロードされて使用準備が整ったときにのみロードおよび検証され、プログラムの開始時には検証されません。ただし、Javaクラスライブラリも通常のJavaクラスであるため、使用時にはロードする必要があります。そのため、Javaプログラムの起動時間は、例えばC++プログラムよりも長くなることがよくあります。
Java Platform, Micro Edition (J2ME)で初めて導入された分割時間検証と呼ばれる手法は、 Java バージョン 6以降、JVM で使用されています。これは、 Java バイトコードの検証を2 つのフェーズに分割します。[ 15 ]
実際には、この方法はJavaコンパイラが持つクラスフローに関する情報を取得し、コンパイルされたメソッドのバイトコードにクラスフロー情報の概要を注釈として付加することで機能します。これにより実行時検証が著しく簡素化されるわけではありませんが、いくつかの簡略化が可能になります。
Javaは言語レベルでマルチスレッド処理を管理できます。マルチスレッド処理により、プログラムは複数のプロセスを同時に実行できるため、複数のプロセッサまたはコアを搭載したコンピュータシステム上で動作するプログラムのパフォーマンスが向上します。また、マルチスレッドアプリケーションは、長時間かかるタスクを実行している間でも、入力に対する応答性を維持できます。
しかし、マルチスレッドを使用するプログラムでは、スレッド間で共有されるオブジェクトに特に注意を払う必要があり、いずれかのスレッドが共有メソッドやブロックを使用する際には、それらへのアクセスをロックする必要があります。ブロックやオブジェクトのロックは、関連するオペレーティングシステムレベルの操作の性質上、時間のかかる操作です(並行性制御とロックの粒度を参照)。
Javaライブラリは、どのメソッドが複数のスレッドで使用されるかを把握できないため、マルチスレッド環境では、必要に応じて常にブロックをロックします。
Java 6 より前は、仮想マシンはプログラムから要求されると、たとえオブジェクトが同時に 2 つのスレッドによって変更されるリスクがない場合でも、常にオブジェクトとブロックをロックしていましたVector。たとえば、この例では、ローカル変数が各add操作の前にロックされ、他のスレッドによって変更されないようにしていました (Vectorは同期化されています)。しかし、ローカル変数はメソッドに対して厳密にローカルであるため、これは不要です。
public String getNames () { final Vector <String> v = new Vector <> (); v.add ( "Me " ) ; v.add ( " You " ) ; v.add ( " Her " ) ; return v.toString ( ) ; }Java 6以降では、コードブロックとオブジェクトは必要な場合にのみロックされるため[ 16 ]、上記の場合、仮想マシンはVectorオブジェクトをまったくロックしません。
Javaはバージョン6u23以降、エスケープ解析のサポートを含むようになりました。[ 17 ]
Java 6より前は、クライアント仮想マシンにおけるレジスタの割り当ては非常に原始的でした(ブロックをまたいで保持されませんでした)。これは、 x86のようにプロセッサレジスタの数が少ないCPU 設計では問題となっていました。操作に使用できるレジスタがなくなると、コンパイラはレジスタからメモリへ(またはメモリからレジスタへ)コピーする必要があり、これには時間がかかります(レジスタへのアクセスはメモリよりもはるかに高速です)。しかし、サーバー仮想マシンはカラーグラフアロケータを使用していたため、この問題は発生しませんでした。
サン社のJDK 6ではレジスタ割り当ての最適化が導入されました。[ 18 ]これにより、(該当する場合)ブロック間で同じレジスタを使用することが可能になり、メモリへのアクセスが削減されました。これにより、一部のベンチマークでは約60%のパフォーマンス向上が報告されています。[ 19 ]
クラスデータ共有(Sun社ではCDSと呼ばれている)は、Javaアプリケーションの起動時間を短縮し、メモリ使用量を削減するメカニズムです。JREがインストールされると、インストーラはシステムJARファイル(すべてのJavaクラスライブラリを含むJARファイルで、rt.jarと呼ばれる)から一連のクラスをプライベートな内部表現にロードし、その表現を「共有アーカイブ」と呼ばれるファイルにダンプします。以降のJVM呼び出しでは、この共有アーカイブがメモリマップインされ、これらのクラスのロードコストが削減され、これらのクラスに関するJVMのメタデータの大部分を複数のJVMプロセス間で共有できるようになります。[ 20 ]
起動時間の改善は、小規模なプログラムでより顕著である。[ 21 ]
ここに挙げた改善点以外にも、Javaの各リリースでは、JVMおよびJavaアプリケーションプログラミングインターフェース(API)において、数多くのパフォーマンス改善が導入されました。
JDK 1.1.6: 初のジャストインタイムコンパイル( Symantecの JIT コンパイラ) [ 2 ] [ 22 ]
J2SE 1.2:世代コレクタの使用。
J2SE 1.3: HotSpotによるジャストインタイムコンパイル。
J2SE 1.4:バージョン1.3と1.4間のパフォーマンス改善に関するSunの概要については、こちらをご覧ください。
Java SE 6:
その他の改善点:
「Java 5 と Java 6 のパフォーマンス改善に関する Sun の概要」も参照してください。[ 26 ]
Java 7 ではいくつかのパフォーマンス改善がリリースされています。Java 6 または Java 7 のアップデートでは、将来のパフォーマンス改善が計画されています。[ 31 ]
Java プログラムと、 C++などの別の言語で書かれた同等のプログラムのパフォーマンスを客観的に比較するには、同一のタスクを実行するプログラムを比較する、慎重かつ綿密に構築されたベンチマークが必要です。JavaのバイトコードコンパイラのターゲットプラットフォームはJava プラットフォームであり、バイトコードは JVM によって解釈されるか、マシン コードにコンパイルされます。他のコンパイラは、ほぼ常に特定のハードウェアおよびソフトウェア プラットフォームをターゲットとし、実行中にほとんど変更されないマシン コードを生成します。これら 2 つの異なるアプローチからは、静的コンパイルと動的コンパイルおよび再コンパイル、実行環境に関する正確な情報の入手可能性など、非常に異なり比較が難しいシナリオが生じます。
JavaはJava仮想マシンによって実行時にジャストインタイムでコンパイルされることが多いが、 C++と同様に事前にコンパイルされる場合もある。ジャストインタイムでコンパイルされた場合、 The Computer Language Benchmarks Gameのマイクロベンチマークは、そのパフォーマンスについて次のことを示している。[ 38 ]
ベンチマークは、多くの場合、数値計算が集中する小規模なプログラムのパフォーマンスを測定します。ごくまれな実際のプログラムでは、Java が C を上回ることがあります。その一例として、Jake2 (オリジナルのGPL C コードを Java に翻訳して書かれたQuake IIのクローン) のベンチマークがあります。Java 5.0 バージョンは、一部のハードウェア構成で、C 版よりも優れたパフォーマンスを発揮します。[ 42 ]データがどのように測定されたかは明記されていません (たとえば、1997 年にコンパイルされたオリジナルの Quake II 実行ファイルが使用されたかどうか。現在の C コンパイラは Quake に対してより良い最適化を実現できるため、これは良くないと考えられます)。しかし、同じ Java ソースコードでも VM を更新するだけで大幅な速度向上を実現できることが指摘されています。これは、100% 静的なアプローチでは不可能なことです。
他のプログラムでは、C++ 版は Java 版よりも大幅に高速に実行できる場合が多く、実際に高速に実行されることが多い。2011 年に Google が行ったベンチマークでは、C++ と Java の間に 10 倍の速度差が見られた。[ 43 ]一方、2012 年に 3D モデリング アルゴリズムを用いて行われた学術的なベンチマークでは、 Windows 環境下でJava 6 JVM が C++ より 1.09 ~ 1.91 倍遅いことが示された。[ 44 ]
Javaや類似の言語で可能な最適化の中には、C++では特定の状況下では不可能なものがあるかもしれない。[ 45 ]
JVMはプロセッサ固有の最適化やインライン展開も実行できます。また、既にコンパイルまたはインライン化されたコードの最適化を解除する機能により、外部ライブラリ関数が関係する場合、静的型付け言語よりも積極的な最適化を実行できる場合があります。[ 46 ] [ 47 ]
JavaとC++間のマイクロベンチマークの結果は、比較対象となる操作によって大きく異なります。例えば、Java 5.0と比較する場合:
マルチコア システム上の Java アプリケーションのスケーラビリティとパフォーマンスは、オブジェクトの割り当て速度によって制限されます。この効果は「割り当ての壁」と呼ばれることもあります。[ 54 ]しかし実際には、最新のガベージ コレクタ アルゴリズムは、ガベージ コレクションを実行するために複数のコアを使用するため、この問題はある程度緩和されます。一部のガベージ コレクタは、1 秒あたり 1 ギガバイトを超える割り当て速度を維持できると報告されており、[ 55 ]数百の CPU コアと数百 GB サイズのヒープにスケーリングしても問題のない Java ベースのシステムも存在します。[ 56 ]
Javaの自動メモリ管理により、ロックレスかつ不変なデータ構造を効率的に利用することが可能になります。これらのデータ構造は、何らかのガベージコレクションなしでは実装が非常に困難、あるいは不可能な場合もあります。Javaは、標準ライブラリのjava.util.concurrentパッケージに、こうした高レベルな構造を多数提供していますが、CやC++など、従来から高性能システム向けに用いられてきた多くの言語には、いまだにこうした構造が欠けています。
Javaの起動時間は、 C、C++、Perl、Pythonなど多くの言語に比べてはるかに遅いことが多い。これは、多くのクラス(特にプラットフォームのクラスライブラリのクラス)が使用される前にロードされる必要があるためである。
同様の一般的なランタイムと比較すると、Windows マシンで実行される小規模プログラムの場合、起動時間はMonoと同程度で、 .NETよりわずかに遅いようです。[ 57 ]
起動時間の大部分は、JVM の初期化やクラスのロードではなく、入出力 (IO) にバウンドする操作によるものと思われる ( rt.jarクラスデータ ファイルだけでも 40 MB あり、JVM はこの大きなファイルから多くのデータを検索する必要がある)。[ 27 ]いくつかのテストでは、新しい分割バイトコード検証方法によりクラスのロードが約 40% 改善されたものの、大規模プログラムの起動の改善は約 5% にとどまったことが示された。[ 58 ]
わずかな改善ではあるものの、単純な処理を実行して終了する小規模なプログラムでは、その効果がより顕著に現れる。なぜなら、Javaプラットフォームのデータ読み込みは、実際のプログラム処理の負荷の何倍にもなる可能性があるからだ。
Java SE 6 Update 10以降、Sun JREにはQuick Starterが付属しており、OS起動時にクラスデータをプリロードすることで、ディスクからではなくディスクキャッシュからデータを取得するようになっています。
Excelsior JETは、この問題を別の角度から解決します。そのスタートアップオプティマイザは、アプリケーション起動時にディスクから読み込む必要のあるデータ量を削減し、読み込みをよりシーケンシャルにします。
2004 年 11 月、コマンドラインから Java プログラムを実行するための「クライアント、プロトコル、およびサーバーであり、JVM の起動オーバーヘッドを発生させない」Nailgun が一般公開されました。 [ 59 ]これにより、スクリプトがJVM をデーモンとして使用し、JVM の起動オーバーヘッドなしで 1 つ以上の Java アプリケーションを実行するオプションが初めて導入されました。Nailgun デーモンは安全ではありません。「すべてのプログラムはサーバーと同じ権限で実行されます」。マルチユーザーのセキュリティが必要な場合、特別な対策を講じない限り、Nailgun は不適切です。アプリケーションごとの JVM 起動がリソース使用の大部分を占めるスクリプトでは、実行時のパフォーマンスが1 ~ 2桁向上します。 [ 60 ]
Javaのメモリ使用量がC++のメモリ使用量よりはるかに多い理由は以下のとおりです。
ほとんどの場合、C++アプリケーションは、Javaの仮想マシン、クラスローディング、自動メモリサイズ変更のオーバーヘッドが大きいため、同等のJavaアプリケーションよりもメモリ消費量が少なくなります。メモリが言語や実行環境の選択において重要な要素となるプログラムでは、費用対効果分析が必要です。
三角関数のパフォーマンスはC言語に比べて劣ります。これは、Javaが数学演算の結果に対して厳密な仕様を持っており、それが基となるハードウェア実装と一致しない可能性があるためです。[ 65 ] x87浮動小数点サブセットでは、Java 1.4以降、sinとcosの引数削減をソフトウェアで行っています[ 66 ]。そのため、範囲外の値ではパフォーマンスが大きく低下します。[ 67 ]
Java Native Interface は高いオーバーヘッドを呼び出し、JVM 上で実行されるコードとネイティブ コードの間の境界を越えることをコストのかかるものにします。[ 68 ] [ 69 ] [ 70 ] Java Native Access (JNA) は、JNI やネイティブ コードを使用せずに、Java コードのみを介して、Javaプログラムからネイティブ共有ライブラリ( Windows ではダイナミック リンク ライブラリ(DLL)) への容易なアクセスを提供します。この機能は、Windows の Platform/Invoke やPython のctypes に匹敵します。アクセスは実行時に動的であり、コード生成は行われません。しかし、コストがかかり、JNA は通常 JNI よりも低速です。[ 71 ]
Swing は、ウィジェットのレンダリングを純粋なJava 2D APIに委任するため、ネイティブのウィジェット ツールキットよりも遅いと認識されてきました。しかし、Swing と、レンダリングをオペレーティングシステムのネイティブ GUI ライブラリに委任するStandard Widget Toolkitのパフォーマンスを比較したベンチマークでは、明確な勝者はなく、結果はコンテキストと環境に大きく依存します。[ 72 ]さらに、Swing を置き換えることを目的とした新しいJavaFXフレームワークは、Swing の多くの固有の問題に対処しています。
一部の人々は、高性能コンピューティング(HPC)におけるJavaのパフォーマンスは、計算集約型ベンチマークではFortranと同程度であると考えているが、JVMはグリッドコンピューティングネットワーク上で集中的な通信を実行するためのスケーラビリティの問題を依然として抱えている。[ 73 ]
しかし、Javaで書かれた高性能コンピューティングアプリケーションはベンチマークコンテストで優勝しています。2008年[ 74 ]と2009年[ 75 ] [ 76 ]には、Apache Hadoop(Javaで書かれたオープンソースの高性能コンピューティングプロジェクト)ベースのクラスタが、1テラバイトと1ペタバイトの整数を最速でソートすることができました。ただし、競合システムのハードウェア構成は固定されていませんでした。[ 77 ] [ 78 ]
Java のプログラムは、他のコンパイル言語のプログラムよりも起動が遅い。[ 79 ] [ 80 ]そのため、特に中国の大学が運営するオンライン審査システムでは、Java プログラムに長い時間制限を設けて、Java を使用する参加者に公平を期している。[ 81 ] [ 82 ] [ 83 ] [ 84 ] [ 85 ]
{{cite web}}: CS1メンテナンス: アーカイブサービスは非推奨になりました (リンク)パフォーマンスの問題に対処する場合、最適化解除は非常に興味深いものです。なぜなら、より積極的な最適化を行うことができるからです...後で実績のある安全な方法に戻ることができると分かっているからです。
エスケープ解析は、Java HotSpotサーバーコンパイラが新しいオブジェクトの使用範囲を分析し、Javaヒープに割り当てるかどうかを決定する手法です。エスケープ解析は、Java SE 6u23以降でサポートされ、デフォルトで有効になっています。
レベルでは、これらのメガバイトすべてをディスクから読み込む必要があり、これは非常に遅い操作です。実際には、ディスクのシーク時間が問題です。大きなファイルを順次読み込むのは比較的速いですが、実際に必要なビットをシークするのはそうではありません。そのため、特定のアプリケーションではこれらの大きなファイルのデータのほんの一部しか必要としないにもかかわらず、ファイル全体をシークしているという事実は、ディスクアクティビティが非常に多いことを意味します。
長時間実行アプリケーションに最適化されたサーバーコンパイラを使用すると、Java は 1.09 ~ 1.91 倍遅いことが実証されています (...)結論として、サーバーコンパイラとこれらの重要な機能で得られた結果は、Java が C++ の有効な代替手段とみなせることを示唆しています
のメソッドを既にインライン化しているときに B が現れたらどうなるでしょうか? ここでも JVM が輝きます。JVM は内部的には基本的に動的言語ランタイムであるため、常に警戒を怠らず、まさにこのようなイベントが発生するのを待ち構えています。そして、本当に素晴らしいのは、状況が変わると JVM は最適化を解除できるということです。これは重要な詳細です。他の多くのランタイムは最適化を一度しか実行できません。C コンパイラは、ビルド中に事前にすべて実行する必要があります。アプリケーションのプロファイルを作成して、それを後続のビルドに渡すことができるものもありますが、コードをリリースすると、基本的に最適化された状態になります。CLR のような他の VM ライクなシステムには JIT フェーズがありますが、それは実行の早い段階 (システムが実行を開始する前かもしれません) で発生し、その後は二度と発生しません。 JVMが最適化を解除して解釈に戻ることができるため、楽観的な姿勢をとる余地が生まれます。つまり、大胆な推測を行い、その後、安全な状態にスムーズに戻り、後で再度試みることができるのです。
がSwingを上回る、あるいはその逆となるような経験則を示すのは難しい。一部の環境(Windowsなど)ではSWTが優れている。他の環境(Linux、 Windowsをホストする
VMware
など)では、Swingとその再描画最適化によりSWTを大幅に上回る。パフォーマンスの差は大きく、どちらの方向にも2倍以上の差が見られることは珍しくない。
まず、さまざまな JVM に対していくつかのマイクロ ベンチマークを実行し、基本的な算術演算の全体的なパフォーマンスが良好であることを示します (...)。この実装を Fortran/MPI の実装と比較すると、計算集約型のベンチマークでは同様のパフォーマンスを示しますが、集約型の通信を実行するとスケーラビリティの問題が依然として発生します。
またはオープンソースプログラムが優勝したのはこれが初めてです。
ハードウェアとオペレーティングシステムの詳細は次のとおりです:(...)Sun Java JDK (1.6.0_05-b13および1.6.0_13-b03) (32ビットおよび64ビット)