返回

使用設定檔引導最佳化來最佳化原生可執行檔

預設情況下,GraalVM Native Image 為 Java 應用程式提供快速啟動和較低的記憶體消耗,並以原生可執行檔的形式執行。您可以透過應用設定檔引導最佳化 (Profile-Guided Optimizations, PGO) 來進一步最佳化此原生可執行檔,以獲得額外的效能提升和更高的吞吐量。

透過 PGO,您可以預先收集設定檔資料,然後將其饋送給 native-image 工具,該工具將使用此資訊來最佳化原生應用程式的效能。一般工作流程如下:

  1. 透過將 --pgo-instrument 選項傳遞給 native-image,來建置檢測的原生可執行檔。
  2. 執行檢測的可執行檔以產生設定檔檔案。預設情況下,會在目前的工作目錄和應用程式關閉時產生 default.iprof 檔案。
  3. 建置最佳化的可執行檔。將會自動選取具有預設名稱和位置的設定檔檔案。或者,您可以透過指定檔案路徑將其傳遞給 native-image 建置器:--pgo=myprofile.iprof

您可以在執行檢測的原生可執行檔時,透過在執行時間傳遞 -XX:ProfilesDumpFile=YourFileName 選項來指定收集設定檔的位置。您也可以透過指定不同的檔案名稱來收集多個設定檔檔案,並在建置時將它們傳遞給 native-image

請注意,執行所有相關的應用程式程式碼路徑,並給予應用程式足夠的時間來收集設定檔,對於擁有完整的設定檔資訊以及最佳效能至關重要。

注意:PGO 在 GraalVM 社群版中不可用。

如需此主題的更多資訊,請參閱設定檔引導最佳化參考文件

執行示範

在示範部分,您將執行使用 Java Streams API 實作查詢的 Java 應用程式。使用者應提供兩個整數引數:迭代次數和資料陣列的長度。應用程式會使用確定性的隨機種子建立資料集,並迭代 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. 透過將 --pgo-instrument 選項傳遞給 native-image,來建置檢測的原生可執行檔
     native-image --pgo-instrument Streams
    
  5. 執行它以收集程式碼執行頻率設定檔
     ./streams 100000 20
    

    請注意,您可以使用小得多的資料大小來進行設定檔。從此次執行收集的設定檔預設會儲存在 default.iprof 檔案中。

  6. 最後,建置最佳化的原生可執行檔。設定檔檔案具有預設名稱和位置,因此將會自動選取
     native-image --pgo Streams
    
  7. 執行此最佳化的原生可執行檔,計時執行時間,以查看系統資源和 CPU 使用率
     time ./streams 100000 200
    

    您應獲得與程式的 Java 版本相當或更快的效能。例如,在具有 16 GB 記憶體和 8 個核心的電腦上,10 次迭代的 TOTAL time 從約 2200 毫秒減少到約 270 毫秒。

本指南示範如何最佳化原生可執行檔,以獲得額外的效能提升和更高的吞吐量。Oracle GraalVM 為建置原生可執行檔提供額外的優勢,例如設定檔引導最佳化 (PGO)。透過 PGO,您可以針對特定工作負載「訓練」您的應用程式,並顯著提升效能。

與我們聯繫