配置文件引导优化 (PGO) 的基本用法

为了解释 PGO 在 GraalVM 原生镜像中的用法,让我们考虑 “生命游戏” 示例应用程序。它是在 4000 x 4000 网格上实现的 Conway's 生命游戏模拟。应用程序以一个指定世界初始状态的文件、一个输出最终状态的文件路径和一个声明要运行的模拟迭代次数的整数作为输入。请注意,这不是一个真实的应用程序,但它可以用作一个很好的示例。

以下是应用程序的源代码,修改自 此资源

import java.io.BufferedReader;
import java.io.FileReader;
import java.io.FileWriter;

public class GameOfLife {

    private static final int M = 4000;
    private static final int N = 4000;

    public static void main(String[] args) {
        new GameOfLife().run(args);
    }

    private void run(String[] args) {
        if (args.length < 3) {
            System.err.println("Too few arguments, need input file, output file and number of generations");
            System.exit(1);
        }

        String input = args[0];
        String output = args[1];
        int generations = Integer.parseInt(args[2]);

        int[][] grid = loadGrid(input);
        for (int i = 1; i <= generations; i++) {
            grid = nextGeneration(grid);
        }
        saveGrid(grid, output);
    }

    static int[][] nextGeneration(int[][] grid) {
        int[][] future = new int[M][N];
        for (int l = 0; l < M; l++) {
            for (int m = 0; m < N; m++) {
                applyRules(grid, future, l, m, getAliveNeighbours(grid, l, m));
            }
        }
        return future;
    }

    private static void applyRules(int[][] grid, int[][] future, int l, int m, int aliveNeighbours) {
        if ((grid[l][m] == 1) && (aliveNeighbours < 2)) {
            // Cell is lonely and dies
            future[l][m] = 0;
        } else if ((grid[l][m] == 1) && (aliveNeighbours > 3)) {
            // Cell dies due to over population
            future[l][m] = 0;
        } else if ((grid[l][m] == 0) && (aliveNeighbours == 3)) {
            // A new cell is born
            future[l][m] = 1;
        } else {
            // Remains the same
            future[l][m] = grid[l][m];
        }
    }

    private static int getAliveNeighbours(int[][] grid, int l, int m) {
        int aliveNeighbours = 0;
        for (int i = -1; i <= 1; i++) {
            for (int j = -1; j <= 1; j++) {
                if ((l + i >= 0 && l + i < M) && (m + j >= 0 && m + j < N)) {
                    aliveNeighbours += grid[l + i][m + j];
                }
            }
        }
        // The cell needs to be subtracted from its neighbors as it was counted before
        aliveNeighbours -= grid[l][m];
        return aliveNeighbours;
    }

    private static void saveGrid(int[][] grid, String output) {
        try (FileWriter myWriter = new FileWriter(output)) {
            for (int i = 0; i < M; i++) {
                for (int j = 0; j < N; j++) {
                    if (grid[i][j] == 0)
                        myWriter.write(".");
                    else
                        myWriter.write("*");
                }
                myWriter.write(System.lineSeparator());
            }
        } catch (Exception e) {
            throw new IllegalStateException();
        }
    }

    private static int[][] loadGrid(String input) {
        try (BufferedReader reader = new BufferedReader(new FileReader(input))) {
            int[][] grid = new int[M][N];
            for (int i = 0; i < M; i++) {
                String line = reader.readLine();
                for (int j = 0; j < N; j++) {
                    if (line.charAt(j) == '*') {
                        grid[i][j] = 1;
                    } else {
                        grid[i][j] = 0;
                    }
                }
            }
            return grid;
        } catch (Exception e) {
            throw new IllegalStateException(e);
        }
    }
}

应用程序性能以经过时间来衡量。假设对应用程序应用更好的优化会使应用程序完成工作负载所需的时间更少。要查看性能差异,您可以通过两种不同的方式运行应用程序:首先是不用 PGO,然后是使用 PGO。

构建应用程序 #

先决条件是安装 Oracle GraalVM。最简单的入门方法是使用 SDKMAN!。有关其他安装选项,请访问 下载部分

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

第一步是将 GameOfLife.java 编译为 class 文件

javac GameOfLife.java

接下来,使用 -o 选项构建应用程序的原生镜像,并指定一个唯一名称

native-image -cp . GameOfLife -o gameoflife-default

现在您可以继续构建支持 PGO 的原生镜像。为此,您首先需要构建一个 “带探测的二进制文件”,它通过添加 --pgo-instrumented 选项并指定不同的名称来生成应用程序运行时行为的配置文件,如下所示

native-image  --pgo-instrument -cp . GameOfLife -o gameoflife-instrumented

现在运行该带探测的二进制文件以收集配置文件。默认情况下,它在退出之前会在当前工作目录中生成一个名为 default.iprof 的文件,但您可以通过在运行带探测的二进制文件时传递 -XX:ProfilesDumpFile 选项来指定配置文件的不同名称和路径。您还应该提供应用程序的标准预期输入:世界初始状态(input.txt)、应用程序将向其打印世界最终状态的文件(output.txt)以及您想要的迭代次数(在本例中为 10)。

./gameoflife-instrumented -XX:ProfilesDumpFile=gameoflife.iprof input.txt output.txt 10

有了包含在 gameoflife.iprof 文件 中的应用程序运行时配置文件后,您终于可以使用 --pgo 选项构建优化的原生可执行文件,并提供收集的配置文件,如下所示。

native-image -cp . GameOfLife -o gameoflife-pgo --pgo=gameoflife.iprof

有了这一切,您就可以继续评估应用程序在不同模式下运行时的运行时性能。

评估性能 #

要评估性能,请使用相同的输入运行应用程序的两个原生可执行文件。您可以使用带有自定义输出格式的 time 命令来衡量可执行文件的经过时间(--format=>> Elapsed: %es)。

注意:在所有测量过程中,CPU 时钟固定为 2.5GHz,以最大程度地减少噪声并提高可重复性。

使用单次迭代运行 #

按照如下所示运行应用程序,以便它只迭代一次

time  ./gameoflife-default input.txt output.txt 1
    >> Elapsed: 1.67s

time  ./gameoflife-pgo input.txt output.txt 1
    >> Elapsed: 0.97s

查看经过时间,您可以看到运行支持 PGO 的原生可执行文件在百分比方面要快得多。考虑到这一点,对于应用程序的单次运行,半秒的差异不会产生很大影响,但如果这是一个经常执行的无服务器应用程序,那么累积的性能增益将开始累积。

使用 100 次迭代运行 #

现在继续使用 100 次迭代运行应用程序。与之前一样,执行的命令和时间输出如下所示

time  ./gameoflife-default input.txt output.txt 100
    >> Elapsed: 24.02s

time  ./gameoflife-pgo input.txt output.txt 100
    >> Elapsed: 13.25s

在两次评估运行中,支持 PGO 的原生可执行文件都显著优于默认的原生构建。在本例中,PGO 提供的改进量并不代表实际应用程序中 PGO 增益,因为此应用程序很小,只做一件事,因此提供的配置文件基于正在测量的完全相同的工作负载。然而,它说明了总体要点:配置文件引导优化使 AOT 编译器能够执行类似于 JIT 编译器的优化。

可执行文件大小 #

在 GraalVM 原生镜像中使用 PGO 的另一个优点是原生可执行文件的大小。要衡量文件的大小,可以使用 Linux du 命令,如下所示。

du -hs gameoflife-default
    7.9M    gameoflife-default

du -hs gameoflife-pgo
    6.7M    gameoflife-pgo

如您所见,支持 PGO 的原生可执行文件比默认的原生构建小约 15%。

这是因为为优化构建提供的配置文件允许编译器区分哪些代码对性能很重要(“热点代码”)以及哪些代码不重要(“冷点代码”,例如错误处理)。有了这种区分,编译器可以决定更多地关注优化热点代码,而对冷点代码则减少或不进行优化。这与 JVM 的方法类似 - 在运行时识别代码的热点部分,并在运行时编译这些部分。主要区别在于原生镜像 PGO 在提前时间进行探测和优化。

进一步阅读 #

与我们联系