戻る

プロファイルガイド付き最適化によるネイティブ実行ファイルの最適化

GraalVM Native Imageは、デフォルトでネイティブ実行ファイルとして実行されるJavaアプリケーションの起動速度の向上とメモリ消費量の削減を実現します。プロファイルガイド付き最適化(PGO)を適用することで、このネイティブ実行ファイルをさらに最適化し、パフォーマンス向上とスループットの向上を図ることができます。

PGOを使用すると、事前にプロファイリングデータを収集し、それを`native-image`ツールに供給できます。ツールはこの情報を使用して、ネイティブアプリケーションのパフォーマンスを最適化します。一般的なワークフローは以下のとおりです。

  1. `native-image`に`--pgo-instrument`オプションを渡して、計装されたネイティブ実行ファイルを作成します。
  2. 計装された実行ファイルを実行して、プロファイルファイルを作成します。デフォルトでは、`default.iprof`ファイルが現在の作業ディレクトリに、アプリケーションのシャットダウン時に生成されます。
  3. 最適化された実行ファイルを作成します。デフォルトの名前と場所にプロファイルファイルがあれば、自動的に選択されます。または、ファイルパスを指定して`native-image`ビルダーに渡すことができます。例:`--pgo=myprofile.iprof`。

計装されたネイティブ実行ファイルを実行する際に、プロファイルの収集場所をランタイムオプション`-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!を使用するのが最も簡単な方法です。その他のインストールオプションについては、ダウンロードセクションを参照してください。

  1. 次のコードを`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;
      }
    }
    
  2. アプリケーションをコンパイルします。
    javac Streams.java
    

    (オプション) パフォーマンスを確認するために、いくつかの引数を指定してデモアプリケーションを実行します。

    java Streams 100000 200
    
  3. クラスファイルからネイティブ実行ファイルを作成し、実行してパフォーマンスを比較します。
     native-image Streams
    

    実行ファイル`streams`が現在の作業ディレクトリに作成されます。同じ引数で実行してパフォーマンスを確認します。

     ./streams 100000 200
    

    このバージョンのプログラムは、GraalVMまたは通常のJDKでの実行よりも遅くなることが予想されます。

  4. `native-image`に`--pgo-instrument`オプションを渡して、計装されたネイティブ実行ファイルを作成します。
     native-image --pgo-instrument Streams
    
  5. 実行してコード実行頻度プロファイルを収集します。
     ./streams 100000 20
    

    はるかに小さいデータサイズでプロファイルを作成できることに注意してください。この実行で収集されたプロファイルは、デフォルトで`default.iprof`ファイルに保存されます。

  6. 最後に、最適化されたネイティブ実行ファイルを作成します。プロファイルファイルはデフォルトの名前と場所にあるため、自動的に選択されます。
     native-image --pgo Streams
    
  7. この最適化されたネイティブ実行ファイルを実行し、実行時間を測定してシステムリソースとCPU使用率を確認します。
     time ./streams 100000 200
    

    プログラムのJavaバージョンと同等かそれ以上の性能が得られるはずです。たとえば、メモリ16GB、8コアのマシンでは、10回の反復の`TOTAL time`が約2200ミリ秒から約270ミリ秒に短縮されました。

このガイドでは、ネイティブ実行ファイルを最適化してパフォーマンス向上とスループット向上を図る方法を示しました。Oracle GraalVMは、プロファイルガイド付き最適化(PGO)など、ネイティブ実行ファイルの構築にさらなるメリットを提供します。PGOを使用すると、特定のワークロードに合わせてアプリケーションを「トレーニング」し、パフォーマンスを大幅に向上させることができます。

お問い合わせ