返回

使用配置文件引导优化优化本机可执行文件

GraalVM Native Image 默认情况下可为 Java 应用程序提供快速启动和更少的内存消耗,并作为原生可执行文件运行。通过应用配置文件引导优化(Profile-Guided Optimization,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 应用程序,该应用程序使用Java Streams API 实现查询。用户需要提供两个整数参数:迭代次数和数据数组的长度。应用程序使用确定性随机种子创建数据集并迭代 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,您可以“训练”您的应用程序以适应特定工作负载并显著提高性能。

联系我们