GraalVM - 编译或运行时优化

构建选项

  • -march=native 如果您的二进制文件将在和编译的机器同一类型cpu下工作,可以打开这个功能,使得GraalVM为你启用更多的CPU功能。

  • -Ob 在开发时构建选项添加这个,可以加快构建速度,但是这样会禁用掉绝大多数的优化。

GC

原生镜像在执行时并不运行在 Java HotSpot VM 上,而是运行在 GraalVM 提供的运行时系统上。该运行时包括所有必要的组件,其中之一就是内存管理。

本机映像在运行时分配的 Java 对象驻留在称为“Java Heap”的区域中。Java Heap是在本机映像启动时创建的,并且在本机映像运行时可能会增大或减小。当堆已满时,会触发垃圾回收以回收不再使用的对象的内存。

为了管理 Java Heap,Native Image 提供了不同的垃圾收集器 (GC) 实现:

  • Serial GC是 GraalVM Native Image 中的默认 GC。它针对低内存占用和较小的 Java 堆大小进行了优化。添加--gc=serial 构建参数,启动这个破GC。

  • G1 GC是一种多线程 GC,经过优化可减少 Stop-the-world 暂停,从而改善延迟,同时实现高吞吐量。要启用它,请将选项传递--gc=G1native-image构建器。目前,G1 垃圾收集器可与 Linux AMD64 和 AArch64 架构上的 Native Image 一起使用。(GraalVM 社区版中不可用。)

  • Epsilon GC(适用于 GraalVM 21.2 或更高版本)是一种无操作垃圾收集器,它不执行任何垃圾收集,因此永远不会释放任何分配的内存。此 GC 的主要用例是运行时间非常短且仅分配少量内存的应用程序。要启用 Epsilon GC,构建参数添加--gc=epsilon 启用该GC。

怎么好用的GC还要付钱,甲骨文要变微软Plus? 当我没说,带G1GC的版本你可以直接下载下来直接用。

SerialGC性能调整

为了调整 GC 性能和内存占用,可以使用以下选项:

  • -XX:MaximumHeapSizePercent- 如果未指定最大 Java 堆大小,则使用物理内存大小的百分比作为最大 Java 堆大小。

  • -XX:MaximumYoungGenerationSizePercent- 年轻代的最大大小占最大 Java 堆大小的百分比。

  • -XX:±CollectYoungGenerationSeparately(自 GraalVM 21.0 起)- 确定完整 GC 是单独收集年轻代还是与老代一起收集。如果启用,这可能会减少完整 GC 期间的内存占用。但是,完整 GC 可能需要更多时间。

  • -XX:MaxHeapFree(自 GraalVM 21.3 起)- 收集后仍为分配保留的可用内存块的最大总大小(以字节为单位),因此不会返回给操作系统。

  • -H:AlignedHeapChunkSize(只能在图像构建时指定) - 堆块的大小(以字节为单位)。

  • -H:MaxSurvivorSpaces(自 GraalVM 21.1 起,只能在镜像构建时指定)- 用于年轻代的幸存者空间数量,即对象晋升到老一代的最大年龄。值为 0 时,年轻代收集中幸存下来的对象将直接晋升到老一代。

  • -H:LargeArrayThreshold(只能在构建映像时指定)- 数组的大小等于或大于该大小时,将在其自己的堆块中分配该数组。被视为较大的数组分配起来更昂贵,但它们永远不会被 GC 复制,这可以减少 GC 开销。

# Build and execute a native image that uses a maximum heap size of 25% of the physical memory
native-image --gc=serial -R:MaximumHeapSizePercent=25 HelloWorld
./helloworld

# Execute the native image from above but increase the maximum heap size to 75% of the physical memory
./helloworld -XX:MaximumHeapSizePercent=75

以下选项-H:InitialCollectionPolicy=BySpaceAndTime仅适用于:

  • -XX:PercentTimeInIncrementalCollection- 确定 GC 应花多少时间进行年轻代收集。默认值为 50,GC 会尝试平衡年轻代收集和完整收集所花费的时间。增加此值将减少完整 GC 的次数,这可以提高性能,但可能会增加内存占用。减少此值将增加完整 GC 的次数,这可以提高内存占用,但可能会降低性能。

G1GC性能调整

G1 GC 是一种自适应垃圾收集器,其默认设置使其无需修改即可高效工作。但是,它可以根据特定应用程序的性能需求进行调整。以下是进行性能调整时可以指定的一小部分选项:

  • -H:G1HeapRegionSize(只能在图像构建时指定) - G1 区域的大小。

  • -XX:MaxRAMPercentage- 如果未指定最大堆大小,则使用物理内存大小的百分比作为最大堆大小。

  • -XX:MaxGCPauseMillis- 最大暂停时间的目标。

  • -XX:ParallelGCThreads- 垃圾收集暂停期间用于并行工作的最大线程数。

  • -XX:ConcGCThreads- 用于并发工作的最大线程数。

  • -XX:InitiatingHeapOccupancyPercent- 触发标记周期的 Java 堆占用率阈值。

  • -XX:G1HeapWastePercent- 收集候选集允许的未回收空间。如果收集候选集的可用空间低于该值,G1 将停止空间回收阶段。

垃圾回收详细打印

执行编译出来的二进制时,可以使用以下选项打印一些有关垃圾回收的信息。打印哪些数据信息取决于所使用的 GC。

  • -XX:+PrintGC- 打印每次垃圾收集的基本信息

  • -XX:+VerboseGC- 可以添加以打印进一步的垃圾收集详细信息

./helloworld -XX:+PrintGC

内存管理

Java Heap

和普通的Java一样他也有配置Java堆的选项

# Build a native image with the default heap settings and override the heap settings at run time
native-image HelloWorld
./helloworld -Xms2m -Xmx10m -Xmn1m

执行编译好的二进制文件时,将根据系统配置和使用的 GC 自动确定合适的 Java 堆设置。 要覆盖此自动机制并在运行时明确设置堆大小,可以使用以下命令行选项:

  • -Xmx- 最大堆大小(以字节为单位)

  • -Xms- 最小堆大小(以字节为单位)

  • -Xmn- 年轻代的大小(以字节为单位)

还可以在镜像构建时预先配置默认堆设置。然后指定的值将在运行时用作默认值:

  • -R:MaxHeapSize(自 GraalVM 20.0 起)- 最大堆大小(以字节为单位)

  • -R:MinHeapSize(自 GraalVM 20.0 起)- 最小堆大小(以字节为单位)

  • -R:MaxNewSize(自 GraalVM 20.0 起)- 年轻代的大小(以字节为单位)

引用压缩

Oracle GraalVM 支持对使用 32 位(而非 64 位)的 Java 对象进行压缩引用。压缩引用默认启用,并且会对内存占用产生很大影响。但是,它们将最大 Java 堆大小限制为 32 GB 内存。如果需要超过 32 GB,则需要禁用压缩引用。

  • -H:±UseCompressedReferences(只能在native-image时指定) - 确定是否使用 32 位 Java 对象引用。

Native Memory

Native Image 能分配独立于 Java 堆外的内存。一个常见用例是java.nio.DirectByteBuffer直接引用Native Memory。

  • -XX:MaxDirectMemorySize- 直接缓冲区分配的最大大小

引导优化

JIT 编译器相对于AOT 编译器的一个优势是它能够分析应用程序的运行时行为。例如,HotSpot 会跟踪语句的每个分支执行的次数if。此信息称为“profile”,会传递给二级 JIT 编译器(Graal)。然后,二级 JIT 编译器会假设该if语句将继续以相同的方式运行,并使用配置文件中的信息来优化该语句。

AOT 编译器通常不具备分析信息,并且通常仅限于静态代码视图。这意味着,除非采用启发式方法,否则 AOT 编译器会认为每个if语句的每个分支在运行时发生的可能性相同;每个方法被调用的可能性与其他方法一样;并且每个循环重复的次数相同。这让 AOT 编译器处于劣势 —如果没有分析信息,就很难生成与 JIT 编译器质量相同的机器代码。

配置文件引导优化 (PGO) 是一种将配置文件信息带给 AOT 编译器的技术,以提高其输出在性能和大小方面的质量。

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

构建带有PGO的二进制文件

进行profile收集需要在构建参数中添加--pgo-instrument 构建一个具有PGO收集的二进制文件(这个过程可能需要很长一段时间),随后我们执行以下代码(GraalVM是我编译出来的二进制文件的名称):

./GraalVM -XX:ProfilesDumpFile=gprofile.iprof

随后我们在构建参数中添加--pgo=/home/fuqiuluo/IdeaProjects/GraalVM/gprofile.iprof 将profile带入构建,使得AOT编译期可以引导优化。

public class Main {
    public static void main(String[] args) {
        long startTime = System.nanoTime();

        int numberOfPrimes = 0;
        for (int i = 2; i < 1_000_000; i++) {
            if (isPrime(i)) {
                numberOfPrimes++;
            }
        }

        long endTime = System.nanoTime();
        long duration = (endTime - startTime);

        System.out.println("Number of primes found: " + numberOfPrimes);
        System.out.println("Time taken (nanoseconds): " + duration);
        System.out.println("Time taken (milliseconds): " + duration / 1_000_000);
    }

    public static boolean isPrime(int num) {
        if (num <= 1) return false;
        if (num == 2) return true;
        if (num % 2 == 0) return false;
        for (int i = 3; i * i <= num; i += 2) {
            if (num % i == 0) return false;
        }
        return true;
    }
}

例如我们有以上代码,这是他在各种情况下的性能表现

GraalVM JIT运行表现

Number of primes found: 78498
Time taken (nanoseconds): 110627247
Time taken (milliseconds): 110

PGO收集时运行时表现

Number of primes found: 78498
Time taken (nanoseconds): 101485196
Time taken (milliseconds): 101

PGO优化后表现

Number of primes found: 78498
Time taken (nanoseconds): 100587815
Time taken (milliseconds): 100

有所提升,但不高,主要是因为优化版本提供的profile文件允许编译器区分哪些代码对性能很重要(即热代码),哪些代码不重要(即冷代码,例如错误处理)。有了这种区分,编译器可以决定更多地关注优化热代码,而较少关注或根本不关注冷代码。这与 JVM 所做的方法类似 - 在运行时识别代码的热门部分并在运行时编译这些部分。主要区别在于 Native Image PGO 会提前进行分析和优化。

在我的这段代码中没有迎合AOT的优化,但是因为native运行,在这种CPU密集的场合AOT的确比JIT要快,https://www.graalvm.org/latest/reference-manual/native-image/optimizations-and-performance/PGO/basic-usage/

官方的这个示例,更突出PGO优化的好处。

https://medium.com/graalvm/profile-guided-optimization-for-native-image-f015f853f9a8

合并来自多个来源的配置文件

PGO 基础架构可让您使用Native Image Configure Tool将多个profile文件合并为一个profile文件。合并配置文件意味着生成的profile文件将包含所提供profile文件中的所有类型、方法和profile文件条目的并集。

用法

要将两个profile文件profile_1.iprofprofile_2.iprof合并为一个名为output_profile.iprof的文件,请使用以下命令:

native-image-configure merge-pgo-profiles --input-file=profile_1.iprof --input-file=profile_2.iprof --output-file=output_profile.iprof
复制

还有一种方法可以使用选项指定目录作为profile文件的来源 --input-dir=<path>。然后它只会在给定目录中搜​​索profile文件,不包括子目录

native-image-configure merge-pgo-profiles --input-dir=my_profiles/ --output-file=output_profile.iprof