プロファイルに基づく最適化によるネイティブ実行可能ファイルの最適化
GraalVMネイティブ・イメージは、デフォルトでネイティブ実行可能ファイルとして動作するJavaアプリケーションに対して、迅速に起動し、メモリー消費が削減されます。プロファイルに基づく最適化(PGO)を適用することで、このネイティブ実行可能ファイルをさらに最適化し、パフォーマンスとスループットを向上させることができます。
PGOを使用すると、プロファイリング・データを事前に収集してからnative-image
ツールにフィードでき、この情報に基づいてネイティブ・アプリケーションのパフォーマンスが最適化されます。一般的なワークフローは次のとおりです:
--pgo-instrument
オプションをnative-image
に渡して、インストゥルメントされたネイティブ実行可能ファイルをビルドします- インストゥルメントされた実行可能ファイルを実行して、プロファイル・ファイルを生成します。デフォルトでは、default.iprofファイルは現在の作業ディレクトリにアプリケーションのシャットダウン時に生成されます。
- 最適化された実行可能ファイルをビルドします。デフォルトの名前と場所のプロファイル・ファイルが自動的に取得されます。または、ファイル・パス
--pgo=myprofile.iprof
を指定して、native-image
ビルダーに渡すことができます。
インストゥルメントされたネイティブ実行可能ファイルの実行時にプロファイルを収集する場所を指定するには、実行時に-XX:ProfilesDumpFile=YourFileName
オプションを渡します。また、異なるファイル名を指定して複数のプロファイル・ファイルを収集し、ビルド時にnative-image
に渡すこともできます。
関連するすべてのアプリケーション・コード・パスを実行し、プロファイルを収集するのに十分な時間をアプリケーションに与えることは、完全なプロファイリング情報を取得し、最高のパフォーマンスを得るために不可欠であることに注意してください。
ノート: PGOは、GraalVM Community Editionでは使用できません。
このトピックの詳細は、プロファイルに基づく最適化のリファレンス・ドキュメントを参照してください。
デモの実行
デモ・パートでは、Java Streams APIで実装された問合せを実行するJavaアプリケーションを実行します。ユーザーは、反復回数とデータ配列の長さの2つの整数引数を指定することが想定されています。アプリケーションは、決定論的な乱数シードを使用してデータ・セットを作成し、10回反復します。各反復とそのチェックサムに要した時間がコンソールに出力されます。
最適化するストリーム式を次に示します:
Arrays.stream(persons)
.filter(p -> p.getEmployment() == Employment.EMPLOYED)
.filter(p -> p.getSalary() > 100_000)
.mapToInt(Person::getAge)
.filter(age -> age > 40)
.average()
.getAsDouble();
PGOを使用して最適化されたネイティブ実行可能ファイルをビルドするには、次のステップに従います。
前提条件
GraalVM JDKがインストール済であることを確認します。最も簡単に始めるには、SDKMAN!を使用します。その他のインストール・オプションについては、「ダウンロード」セクションを参照してください。
- 次のコードをStreams.javaという名前のファイルに保存します:
import java.util.Arrays; import java.util.Random; public class Streams { static final double EMPLOYMENT_RATIO = 0.5; static final int MAX_AGE = 100; static final int MAX_SALARY = 200_000; public static void main(String[] args) { int iterations; int dataLength; try { iterations = Integer.valueOf(args[0]); dataLength = Integer.valueOf(args[1]); } catch (Throwable ex) { System.out.println("Expected 2 integer arguments: number of iterations, length of data array"); return; } Random random = new Random(42); Person[] persons = new Person[dataLength]; for (int i = 0; i < dataLength; i++) { persons[i] = new Person( random.nextDouble() >= EMPLOYMENT_RATIO ? Employment.EMPLOYED : Employment.UNEMPLOYED, random.nextInt(MAX_SALARY), random.nextInt(MAX_AGE)); } long totalTime = 0; for (int i = 1; i <= 20; i++) { long startTime = System.currentTimeMillis(); long checksum = benchmark(iterations, persons); long iterationTime = System.currentTimeMillis() - startTime; totalTime += iterationTime; System.out.println("Iteration " + i + " finished in " + iterationTime + " milliseconds with checksum " + Long.toHexString(checksum)); } System.out.println("TOTAL time: " + totalTime); } static long benchmark(int iterations, Person[] persons) { long checksum = 1; for (int i = 0; i < iterations; ++i) { double result = getValue(persons); checksum = checksum * 31 + (long) result; } return checksum; } public static double getValue(Person[] persons) { return Arrays.stream(persons) .filter(p -> p.getEmployment() == Employment.EMPLOYED) .filter(p -> p.getSalary() > 100_000) .mapToInt(Person::getAge) .filter(age -> age >= 40).average() .getAsDouble(); } } enum Employment { EMPLOYED, UNEMPLOYED } class Person { private final Employment employment; private final int age; private final int salary; public Person(Employment employment, int height, int age) { this.employment = employment; this.salary = height; this.age = age; } public int getSalary() { return salary; } public int getAge() { return age; } public Employment getEmployment() { return employment; } }
- アプリケーションをコンパイルします:
javac Streams.java
(オプション)デモ・アプリケーションを実行し、いくつかの引数を指定してパフォーマンスを監視します。
java Streams 100000 200
- クラス・ファイルからネイティブ実行可能ファイルをビルドし、それを実行してパフォーマンスを比較します:
native-image Streams
実行可能ファイルstreamsは、現在の作業ディレクトリに作成されます。ここで、同じ引数を使用して実行し、パフォーマンスを確認します:
./streams 100000 200
このバージョンのプログラムは、GraalVMのJDKや通常のJDKよりも動作が遅くなることが予想されます。
--pgo-instrument
オプションをnative-image
に渡して、インストゥルメントされたネイティブ実行可能ファイルをビルドします:native-image --pgo-instrument Streams
- これを実行して、コード実行頻度プロファイルを収集します:
./streams 100000 20
はるかに小さいデータ・サイズでプロファイルできることに注意してください。この実行から収集されたプロファイルは、デフォルトでdefault.iprofファイルに格納されます。
- 最後に、最適化されたネイティブ実行可能ファイルをビルドします。プロファイル・ファイルにはデフォルトの名前と場所があるため、自動的に取得されます:
native-image --pgo Streams
- この最適化されたネイティブ実行可能ファイルを実行タイミングを計って実行し、システム・リソースおよびCPU使用率を確認します:
time ./streams 100000 200
Javaバージョンのプログラムと同等、またはそれ以上のパフォーマンスが得られるはずです。たとえば、16GBのメモリーと8コアを持つマシンでは、10回の反復の
TOTAL time
が最大2200から最大270ミリ秒に削減されました。
このガイドでは、ネイティブ実行可能ファイルを最適化して、パフォーマンスとスループットを向上させる方法を紹介しました。Oracle GraalVMには、プロファイルに基づく最適化(PGO)など、ネイティブ実行可能ファイルをビルドすることに関して追加の利点があります。PGOを使用すると、特定のワークロードに対してアプリケーションを「トレーニング」し、パフォーマンスを大幅に向上させることができます。