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

为了解释 PGO 在 GraalVM Native Image 上下文中的用法,让我们考虑“生命游戏”示例应用程序。它是在 4000x4000 网格上康威生命游戏模拟的一个实现。该应用程序将指定世界初始状态的文件、输出最终状态的文件路径以及声明运行模拟迭代次数的整数作为输入。请注意,这不是一个说明真实世界应用程序的示例,但它应该可以很好地作为一个例子。

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

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 编译成一个类文件

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 Native Image 中使用 PGO 的另一个优势是本机可执行文件的大小。要测量文件大小,您可以使用 Linux du 命令,如下所示。

du -hs gameoflife-default
    7.9M    gameoflife-default

du -hs gameoflife-pgo
    6.7M    gameoflife-pgo

如您所见,PGO 优化的本机可执行文件比默认的本机构建小约 15%。

这是因为为优化构建提供的配置文件允许编译器区分哪些代码对性能很重要(“热代码”),哪些不重要(“冷代码”,如错误处理)。有了这种区分,编译器可以决定更侧重于优化热代码,而较少或完全不优化冷代码。这与 JVM 的方法类似——在运行时识别代码的热点部分并在运行时编译这些部分。主要区别在于 Native Image PGO 在编译前就完成了剖析和优化。

延伸阅读 #

联系我们