返回

使用 Profile-Guided Optimizations 优化原生可执行文件

GraalVM Native Image 默认情况下会为运行为原生可执行文件的 Java 应用程序提供快速启动和更低的内存消耗。您可以通过应用 Profile-Guided Optimizations (PGO) 来进一步优化此原生可执行文件,以获得更高的性能和吞吐量。

使用 PGO,您可以预先收集分析数据,然后将其提供给 native-image 工具,该工具将使用这些信息来优化原生应用程序的性能。一般工作流程如下:

  1. 通过将 --pgo-instrument 选项传递给 native-image 来构建一个带 instrumentation 的原生可执行文件。
  2. 运行带 instrumentation 的可执行文件以生成分析文件。默认情况下,default.iprof 文件将在应用程序关闭时生成在当前工作目录中。
  3. 构建一个优化的可执行文件。分析文件将使用默认名称和位置自动提取。或者,您可以通过指定文件路径将其传递给 native-image 构建器:--pgo=myprofile.iprof

您可以在运行带 instrumentation 的原生可执行文件时通过传递 -XX:ProfilesDumpFile=YourFileName 选项来指定收集分析文件的目录。您也可以通过指定不同的文件名来收集多个分析文件,并在构建时将它们传递给 native-image

请注意,为了获得完整的分析信息并因此获得最佳性能,执行所有相关的应用程序代码路径并给予应用程序足够的时间来收集分析数据至关重要。

注意:GraalVM Community Edition 中不提供 PGO。

Profile-Guided Optimization 参考文档 中查找有关此主题的更多信息。

运行演示

对于演示部分,您将运行一个 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 来构建一个带 instrumentation 的原生可执行文件
     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 为构建原生可执行文件提供了额外的优势,例如 Profile-Guided Optimizations (PGO)。使用 PGO,您可以针对特定工作负载“训练”应用程序,从而显着提高性能。

与我们联系