- 適用於 JDK 23 的 GraalVM (最新)
- 適用於 JDK 24 的 GraalVM (搶先體驗)
- 適用於 JDK 21 的 GraalVM
- 適用於 JDK 17 的 GraalVM
- 封存
- 開發版本
設定檔導向最佳化的基本用法
為了說明 PGO 在 GraalVM 原生映像檔中的用法,讓我們考慮「生命遊戲」範例應用程式。這是一個在 4000 x 4000 網格上實作的 康威生命遊戲模擬。此應用程式將一個指定世界初始狀態的檔案、一個輸出最終狀態的檔案路徑,以及一個宣告要執行多少次模擬迭代的整數作為輸入。請注意,這不是一個現實世界的應用程式範例,但它應該可以作為一個不錯的範例。
以下是此應用程式的原始碼,修改自此資源。
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
完成所有這些設定後,您可以繼續評估以不同模式執行的應用程式的執行時效能。
評估效能 #
若要評估效能,請使用相同的輸入執行應用程式的兩個原生可執行檔。您可以使用具有自訂輸出格式 (--format=>> Elapsed: %es
) 的 time
命令來測量可執行檔的經過時間。
注意:CPU 時脈在所有測量期間固定為 2.5 GHz,以盡量減少雜訊並提高再現性。
以單一迭代執行 #
如下所示執行應用程式,使其只迭代一次
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 會提前進行設定檔分析和最佳化。