我该如何编写代码来提示JVM使用向量操作?

21
有点相关的问题,已经有一年了:任何JVM的JIT编译器是否会生成使用向量化浮点指令的代码? 前言:我正在尝试在纯Java中进行此操作(没有JNI到C++,没有GPGPU工作等)。 我已经进行了分析,大部分处理时间来自于此方法中的数学运算(可能是95%的浮点数学和5%的整数数学)。 我已经将所有Math.xxx()调用简化为足够好的近似值,因此现在大部分数学都是浮点乘法加上少量加法。
我有一些处理音频的代码。 我一直在进行微调,并且已经取得了巨大的进展。 现在,我正在研究手动循环展开,看看是否有任何好处(至少手动展开2个时,我看到了大约25%的改进)。 当尝试手动展开4个时(这已经变得非常复杂,因为我正在展开嵌套循环的两个循环),我想知道是否有任何方法可以提示jvm在运行时可以使用向量操作(例如SSE2,AVX等)。 每个音频样本可以完全独立于其他样本计算,这就是为什么我已经能够看到25%的改进(减少了对浮点计算的依赖)。
例如,我有4个浮点数,分别用于循环展开的4个部分计算值。 我如何声明和使用这些浮点数是否重要? 如果我将其设置为float[4],是否会提示jvm它们彼此不相关,而不是使用float,float,float,float,甚至是一个包含4个public float的类? 是否有一些我无意中做的事情会破坏我的代码被向量化的机会?

我在网上看到有关“正常”编写代码的文章,因为编译器/JVM知道常见模式以及如何进行优化,而偏离这些模式可能意味着较少的优化。然而,在这种情况下,即使将循环展开两次也不会像它所做的那样大幅提高性能,因此我想知道是否还有其他我可以做(或者至少不要做)来提高成功的机会。我知道编译器/JVM只会变得更好,因此我还要谨慎地避免做一些未来会对我造成伤害的事情。

编辑说明:将循环展开四次比将循环展开两次再增加了约25%的性能,因此我真的认为如果JVM支持它(或者已经在使用),矢量操作会在我的情况下有所帮助。

谢谢!


@maaartinus 感谢提供的链接。内部循环通常会执行10到300次,具体取决于一些用户选择(通常在30-40左右)。然而,外部循环会执行数万次甚至数十万次。我尝试了只展开内部循环,但它增加了执行时间。我尝试了只展开外部循环,它确实减少了执行时间,但只有一点点。我猜测当两者都展开时,CPU可以更轻松地挑选出依赖链。 - user450775
我的想法是:当内部循环执行10次时,其中的每个指令的权重都是外部指令的10倍;因此我会忽略外部指令。我只能猜测,但我敢打赌JVM会自动展开循环(有时甚至过度),而减速只是JIT故障(它以不同方式优化两个等效块,导致结果时间差异很大)。我担心查看生成的汇编代码是唯一的方法。 - maaartinus
你明确排除了GPGPU(我想知道为什么...),但也许你应该简单地看一下https://code.google.com/p/aparapi/:它将使用OpenCL进行计算(这可以是GPU或CPU!),或者在没有OpenCL可用时使用Java线程池。 - Marco13
@Marco13 我不包括GPGPU选项,因为虽然是的,这几乎是GPU工作的完美候选者,但它抵消了Java“一次编写,到处运行”的巨大优势。我想让它在纯Java中尽可能地高效。目标不是“我需要尽可能节省时间?”(否则我会用C编写并使用CUDA / OpenCL),而是“如何使Java尽可能高效?”其中一个问题是是否有一种方法可以提示jvm向量优化将有所帮助。感谢链接!我之前没有听说过。 - user450775
还要注意的是,JIT编译是由在启动时设置的JVM选项控制的(供应商特定)。您可以考虑学习这些内容,甚至比较IBM、Oracle、Azul和Gjc,看看哪个做得比其他人更好。 - Thorbjørn Ravn Andersen
显示剩余3条评论
2个回答

2
如何使用纯Java(没有JNI到C ++,没有GPGPU工作等)进行音频处理并使用向量操作(例如SSE2,AVX等)?
Java是一种高级语言(Java中的一条指令会生成许多硬件指令),它的设计不适用于实时操作高数据量的任务(例如垃圾回收内存管理)。
通常有专门的硬件优化特定角色(例如图像处理语音识别),这很多次通过几个简化的处理管道并行处理。
还有专门用于此类任务的编程语言,主要是硬件描述语言汇编语言
即使是被认为快速的语言C++,也不会自动使用一些超级优化的硬件操作。它可能只是在某些地方内联几种手工制作的汇编语言方法之一。
因此,我的答案是,“可能没有办法”指示JVM为您的代码使用一些硬件优化(例如SSE),即使有一些Java语言运行时仍然具有太多其他因素会减慢您的代码。 使用专门设计用于此任务的低级语言并将其链接到高级逻辑的Java中。
编辑:根据评论添加更多信息
如果您确信高级“编写一次,到处运行”的语言运行时确实应该为您执行大量低级别的优化,并自动将您的高级别代码转换为优化的低级别代码,那么JIT编译器优化的方式取决于Java虚拟机的实现。其中有很多。
如果是 Oracle JVM(HotSpot),您可以通过下载源代码开始查找答案,以下文件中会出现文本SSE2
  • openjdk/hotspot/src/cpu/x86/vm/assembler_x86.cpp
  • openjdk/hotspot/src/cpu/x86/vm/assembler_x86.hpp
  • openjdk/hotspot/src/cpu/x86/vm/c1_LIRGenerator_x86.cpp
  • openjdk/hotspot/src/cpu/x86/vm/c1_Runtime1_x86.cpp
  • openjdk/hotspot/src/cpu/x86/vm/sharedRuntime_x86_32.cpp
  • openjdk/hotspot/src/cpu/x86/vm/vm_version_x86.cpp
  • openjdk/hotspot/src/cpu/x86/vm/vm_version_x86.hpp
  • openjdk/hotspot/src/cpu/x86/vm/x86_32.ad
  • openjdk/hotspot/src/os_cpu/linux_x86/vm/os_linux_x86.cpp
  • openjdk/hotspot/src/share/vm/c1/c1_GraphBuilder.cpp
  • openjdk/hotspot/src/share/vm/c1/c1_LinearScan.cpp
  • openjdk/hotspot/src/share/vm/runtime/globals.hpp
他们是用C++和汇编语言编写的,所以无论如何你都需要学习一些低级语言才能阅读它们。
我认为即使有500美元的赏金,我也不会深入探究。在我看来,这个问题基于错误的假设。

2
我知道Java是一种高级语言,也知道你可以使用JNI调用C/C++,但我说这不是一个选项。Java是“编写一次,到处运行”的,而使用JNI完全抵消了这个巨大的好处。此外,有一些C++编译器会“自动地”使用超级优化操作——这称为自动向量化,一些编译器(例如Intel编译器)在这方面做得相当不错(尽管显然并不完美)。感谢您的回复和花时间链接到几个不同的主题,但它并没有真正回答我的问题。 - user450775
1
另外,我知道编译后的代码不会直接编译成例如SSE指令,但由于这是代码路径中的热点,因此它是JIT编译的一个很好的候选项,我想知道是否有任何方法可以让JVM看到这个部分应该被JIT化,以便它可以利用向量操作。 - user450775

1

Hotspot上的SuperWord优化受到限制且相当脆弱。它们的限制在于通常落后于C/C++编译器所提供的功能,并且它们依赖于特定的循环形状(并且仅支持某些CPU)。

我理解您想要编写一次运行任何地方的代码。听起来您已经有了一个纯Java的解决方案。您可能需要考虑为已知的流行平台提供可选实现,以补充那个“在某些地方快速”的实现,这已经是事实了。

如果有一些代码,更具体的反馈就会更加困难。我建议您将涉及的循环放入JMH基准测试中进行展示。这样可以轻松分析和讨论。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接