Java 18:Vector API自动加速

22-12-10 banq

我们需要了解提前机器代码生成AOT和即时机器代码生成JIT之间的根本区别:

提前机器代码生成:
提前 (AoT) 机器代码生成发生在编译时。这种技术用于 C++ 等语言。在编译过程中,源代码被转录为机器代码。机器代码取决于目标平台,例如 x86、Itanium、ARM、M1。因此,代码仅在编译期间被触及并转录为目标平台的汇编规范。

即时机器代码生成:
相反,即时 (JiT) 机器代码生成的工作方式稍微复杂一些。不是将代码直接转录为目标平台的汇编语言,而是将代码转录为称为字节码的中间代码。Java 字节码是一种中间表示,涵盖了JVM 指令集,同时结合了一些编译器优化。
JIT 编译器读取字节码并为所需平台(x86、ARM 等)发出正确的汇编指令。此外,它可以根据程序流程的知识应用一些更具体的优化。

JIT 编译器结合了两种机器代码翻译方法:解释和提前编译。它负责将字节码翻译成特定于平台的指令。通过添加运行时知识,可以进行特定的优化,这在编译时并不明显。这有助于进一步优化代码并提高运行时性能,优于更传统的语言。

JIT比AOT好在哪里?
了解了即时编译器的属性后,人们可能会问与传统的提前编译相比有什么好处。让我们用一个简单的例子来探讨这个问题:

public void randomLoop(){ 
    var rnd = new Random(); 
    int maxItems = rnd.nextInt(); 
    for(int i = 0; i < maxItems; i++){ 
        doSomethingWith(i); 
    } 
}

AOT 编译器没有机会知道循环将执行多少次迭代。
另一方面,即时编译器能准确地知道maxItems有多大——并且可以针对小型、中型或大型循环进行优化!
(与 Ahead-of-Time 编译相比,此片段仅显示运行时的知识收益。当然,不能保证任何即时编译器的改进。)

循环展开是一种优化技术,用于最小化循环开销,例如边界检查、变量增加。不是检查循环计数器 (i) 并随着每个元素增加它,而是在一个循环体中完成多个元素,以最大程度地减少开销:

public void normalLoop(){ 
    for(int i = 0; i < 128; i++){ 
        doSomethingWith(i); 
    } 
}
public void unrolledLoop(){ 
    for(int i = 0; i < 32; i+=4){ 
        doSomethingWith(i+0); 
        doSomethingWith(i+1); 
        doSomethingWith(i+2); 
        doSomethingWith(i+3); 
    } 
}


只要迭代次数已知,这是在提前编译期间使用的普遍技术。(曾经问过自己为什么 C++ const 变量如此重要——现在您知道了!)在即时编译器中,编译器知道运行时的循环大小,因此可以根据变量的大小强制展开循环。

向量化/矢量化:
另一种潜在的方法是自动矢量化(Auto-vectorization)。基于矢量/向量的指令触发处理器的特定 SIMD(单指令,多数据)寄存器,这些寄存器可以在同一指令周期内对多个元素执行一个操作。截取的simpleSum代码是一个可以通过矢量化优化的完美示例。使用 Javas Vector API,我们可以编写与平台无关但针对 SIMD 优化的代码。

是自动矢量化还是循环展开?
我们现在已经讨论了两种策略来优化带有算术运算的简单循环。最后,是时候证明哪些 JIT 优化消除了手动调整代码的潜在加速。
JVM 由各种诊断标志组成,可以将发出的汇编代码存储到日志文件中。此外,可以打开和关闭某些 JIT 功能。这种启用和禁用 JIT 功能有助于研究优化技术及其对代码运行时的影响。
JIT默认生成的机器代码包含向量并行和循环展开。

结论
通过深入了解 JVM,令人印象深刻的是需要将循环展开和自动矢量化相结合才能产生最大吞吐量。
可以通过比较两个机器代码块来估计假设的计算效率改进:
没有循环展开的版本每个数组项需要 6 条指令,而有循环展开的版本每四个项需要 15 条指令;因此,将 4 项的 24 条指令简化估计为 4 项的 15 条指令会导致“非常”假设提升 1.6 倍。这纯粹是理论上的!

矢量化应该贡献 4x (AVX-2) 的理论性能优势。但是,预计在这种情况下不会看到这样的提升。与计算操作的原始性质相比,通过寄存器移动数据的开销非常昂贵。因此,测得的加速比较低。将循环展开和矢量化相结合时,速度提高了 2 倍,这表明英特尔 x86 架构受益于 SIMD 模式下更长的指令步幅——没有比较指令或跳转指令的中断。CPU 在其向量寄存器中不间断运行的时间越长,加速可能就越高。这导致了向量指令存在“冷启动”的结论,这会使处理器暂停一段时间。

详细测试点击标题