Java乘法优化

3

我目前正在编写的代码将有成千上万次迭代,并想知道现代Java编译器在优化时是否会自动处理中间值转换为汇编语言。例如,在循环中我有以下代码(简化):

arrayA[i] += doubleA*doubleB;
arrayB[i] += doubleA*doubleB;

现代Java编译器是否足够“智能”,可以将doubleA*doubleB存储到乘法寄存器中(然后在第二个数组中继续从乘法寄存器中读取,避免第二次浮点运算)?还是说我最好采用以下方法:

double product = doubleA*doubleB;
arrayA[i] += product;
arrayB[i] += product;

对于第二个选项,我主要关注的是Java的垃圾收集器每次处理产品变量时的开销。


1
你会希望任何像样的JIT编译器都能够管理CSE。此外,它不会为从未引用的本地临时变量动态分配任何空间。(对于x86,它将保留在寄存器中,或者在堆栈内存中,如果必须溢出/重新加载,则完全不在堆上)。但是,除非您查看汇编代码或进行一些基本的分析以查看是否有GC工作,否则您无法确定如果您以DRY方式编写它(不要重复自己),这更接近于最终要执行的机器代码。 - Peter Cordes
1
当然,如果您真的关心性能,我不确定当前的JVM JIT编译器是否知道如何使用SSE2或AVX进行自动向量化,以便在单个指令中使用32字节的加载/存储和SIMD vaddpd执行四个打包的double加法,以尽可能快地完成CPU的工作。 - Peter Cordes
@Bubletan:因为double是一种原始类型,根本不是一个Object,对吧?所以JVM不可能把它放在堆上。 - Peter Cordes
只要它是一个局部变量,是的。当然,字段会与对象一起存储在堆中。 - Bubletan
2
总之,以任何你喜欢的方式写都可以。采用第一种方式,任何一个好的编译器都会确保 doubleA*doubleB 只被计算一次。如果你不信任编译器(或者只是觉得它看起来更好),那就采用第二种方式写,因为不会有任何垃圾回收开销。 - Kevin Anderson
2个回答

2
如果您运行代码数百万次,那么代码将很可能被JIT编译。如果您想查看JIT输出,并验证它是否被本地编译,您可以使用JVM标志启用该功能(您还需要预先编译库(由于许可问题,该库不会预装))。
当JIT将代码编译为本机机器代码时,它通常会对代码进行优化。还有一个标志,随着使用次数的增加,它会越来越多地进行优化。值得注意的是,直到函数执行了大约10,000次,JIT编译才会发生,不幸的是,没有办法在程序启动时强制JIT编译代码。假设JIT不应该有任何开销,它可能会在后台的另一个线程上编译代码,然后在完成时注入本机代码(JIT编译仍然只需要不到半秒钟)。
关于将结果存储为double类型,这不会产生任何负面的性能影响。而且你也不需要担心垃圾回收(GC)的问题,因为它是一个原始类型,声明在堆栈上并在作用域退出后弹出(变量将在下一个循环迭代中重新声明)。

但在这段时间内,一个提前优化的循环即使没有使用SIMD或者由于内存瓶颈无法每个时钟周期维持16字节的加载/存储,也可以在4GHz CPU上轻松完成大约10亿次循环迭代。顺便说一句,JIT版本不会在每个循环迭代中实际调整CPU堆栈指针寄存器;那样做太愚蠢了。逻辑上变量进入和退出作用域,但在汇编中它可能只存在于寄存器中,或者为整个函数保留堆栈内存。 - Peter Cordes

0

你几乎永远不会知道jit的作用,但是你可以通过javap轻松查看字节码。如果javac/ide没有对其进行优化,我不会假设jit会这样做。只需编写良好的代码,这样更容易阅读。


2
如何在JVM中查看JIT编译的代码?展示了如何使用HotSpot JIT内置选项查看Sun/Oracle JVM生成的实际机器代码/汇编代码。这使得操作并不太困难。但是,在这种情况下,您可以安全地编写良好的代码,而无需为了获得良好的性能而与语言作斗争。 - Peter Cordes
您可以在生产系统中获取任何JAR文件(或该构建的任何副本),并离线使用javap,但是您无法实际上在生产环境中使用hsdis插件和JVM选项来捕获所需的ASM代码(特别是在反编译和重新编译事件发生时),这几乎不可能。但是在本地执行此类检查是一个好主意!谢谢! - user2023577

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