flo*_*oon 3 java floating-point optimization performance
我最初想用Java中的浮点性能优化来测试一些不同的东西,即除法5.0f
和乘法之间的性能差异0.2f
(乘法似乎在没有预热的情况下更慢,但分别在1.5倍左右更快).
在研究结果后,我注意到我忘记添加一个预热阶段,正如在进行性能优化时经常建议的那样,所以我添加了它.而且,令我完全惊讶的是,在多次测试运行中,平均速度提高了约25倍.
我用以下代码测试了它:
public static void main(String args[])
{
float[] test = new float[10000];
float[] test_copy;
//warmup
for (int i = 0; i < 1000; i++)
{
fillRandom(test);
test_copy = test.clone();
divideByTwo(test);
multiplyWithOneHalf(test_copy);
}
long divisionTime = 0L;
long multiplicationTime = 0L;
for (int i = 0; i < 1000; i++)
{
fillRandom(test);
test_copy = test.clone();
divisionTime += divideByTwo(test);
multiplicationTime += multiplyWithOneHalf(test_copy);
}
System.out.println("Divide by 5.0f: " + divisionTime);
System.out.println("Multiply with 0.2f: " + multiplicationTime);
}
public static long divideByTwo(float[] data)
{
long before = System.nanoTime();
for (float f : data)
{
f /= 5.0f;
}
return System.nanoTime() - before;
}
public static long multiplyWithOneHalf(float[] data)
{
long before = System.nanoTime();
for (float f : data)
{
f *= 0.2f;
}
return System.nanoTime() - before;
}
public static void fillRandom(float[] data)
{
Random random = new Random();
for (float f : data)
{
f = random.nextInt() * random.nextFloat();
}
}
Run Code Online (Sandbox Code Playgroud)
没有预热阶段的结果:
Divide by 5.0f: 382224
Multiply with 0.2f: 490765
Run Code Online (Sandbox Code Playgroud)
结果与热身阶段:
Divide by 5.0f: 22081
Multiply with 0.2f: 10885
Run Code Online (Sandbox Code Playgroud)
我无法解释的另一个有趣的变化是什么操作更快(转换与乘法).正如前面提到的那样,如果没有热身,分区看起来会更快一点,而热身赛的速度似乎要慢两倍.
我尝试添加一个初始化块,将值设置为随机的值,但它不会影响结果,也没有添加多个预热阶段.方法运行的数字是相同的,所以这不是原因.
这种行为的原因是什么?什么是这个预热阶段以及它如何影响性能,为什么在预热阶段操作会更快,为什么操作更快?
Chr*_*s K 12
在热身之前,Java将通过解释器运行字节代码,想想如何编写一个可以在java中执行java字节代码的程序.预热后,热点将为您正在运行的cpu生成本机汇编程序; 利用该cpus功能集.两者之间存在显着的性能差异,解释器将为单字节代码运行许多cpu指令,其中热点生成本机汇编代码,就像gcc在编译C代码时所做的那样.这就是除法和乘法的时间之间的差异最终会降低到运行的CPU,它只是一个cpu指令.
问题的第二部分是热点还记录了衡量代码运行时行为的统计信息,当它决定优化代码时,它将使用这些统计信息来执行在编译时不一定可能的优化.例如,它可以降低空检查,分支错误预测和多态方法调用的成本.
简而言之,必须放弃预热的结果.
Brian Goetz 在这里写了一篇非常好的文章.
========
附录:概述"JVM热身"的含义
JVM'热身'是一个松散的短语,并且不再严格地说是JVM的单个阶段或阶段.人们倾向于使用它来指代在将JVM字节代码编译为本机字节代码后JVM性能稳定的概念.事实上,当一个人开始在表面下划伤并深入研究JVM内部时,很难不被Hotspot为我们做多少所打动.我的目标只是为了让您更好地了解Hotspot在演出中的表现,有关详细信息,我建议您阅读Brian Goetz,Doug Lea,John Rose,Cliff Click和Gil Tene(以及其他许多人)的文章.
如前所述,JVM首先通过其解释器运行Java.虽然严格来说不是100%正确,但可以将解释器视为大型switch语句和循环遍历每个JVM字节代码(命令)的循环.switch语句中的每个case都是一个JVM字节代码,例如将两个值一起添加,调用方法,调用构造函数等等.迭代的开销和跳转命令非常大.因此,执行单个命令通常会使用超过10倍的汇编命令,这意味着硬件必须执行速度慢10倍以上,因此更多的命令和缓存将受到此解释器代码的污染,理想情况下我们更愿意关注我们的实际程序.回想一下Java的早期时代,Java赢得了非常缓慢的声誉; 这是因为它最初只是一种完全解释的语言.
后来JIT编译器被添加到Java中,这些编译器会在调用方法之前将Java方法编译为本机CPU指令.这消除了解释器的所有开销,并允许在硬件中执行代码.虽然硬件中的执行速度要快得多,但这种额外的编译在Java启动时创建了一个停顿.这部分是"热身阶段"的术语占据的部分原因.
将热点引入JVM是游戏规则的改变者.现在,JVM启动速度更快,因为它将开始使用其解释器运行Java程序,并且各个Java方法将在后台线程中编译并在执行期间即时交换.本机代码的生成也可以用于不同的优化级别,有时使用严格说来不正确的非常激进的优化,然后在必要时动态地去优化和重新优化以确保正确的行为.例如,类层次结构意味着要确定将调用哪个方法的成本很高,因为Hotspot必须搜索层次结构并找到目标方法.Hotspot在这里可以变得非常聪明,并且如果它注意到只加载了一个类,那么它可以假设总是如此,并且优化和内联方法本身.如果另一个类被加载,现在告诉Hotspot实际上在两个方法之间做出了决定,那么它将删除其先前的假设并在运行中重新编译.可以在不同情况下进行的优化的完整列表令人印象深刻,并且不断变化.Hotspot能够记录有关其运行环境的信息和统计信息,以及当前正在经历的工作负载,使得执行的优化非常灵活和动态.实际上,在单个Java进程的生命周期内,很可能随着工作负载的性质的变化,该程序的代码将被重新生成多次.可以说,Hotspot比传统的静态编译具有更大的优势,这也是很多Java代码可以被认为与编写C代码一样快的原因.它还使得理解微基准测试变得更加困难; 事实上,它使得Oracle的维护人员更难以理解,使用和诊断问题,这使得JVM代码本身变得更加困难.花一点时间向那些人提出品脱,Hotspot和JVM作为一个整体是一个梦幻般的工程胜利,在人们说它无法完成的时候,它已经脱颖而出.值得记住的是,因为十年左右它是一个相当复杂的野兽;)
因此,考虑到上下文,总的来说,我们指的是在微基准测试中将JVM加热为运行目标代码超过10k次并抛出结果,以便让JVM有机会收集统计信息并优化其中的"热区域".码.10k是一个神奇的数字,因为Server Hotspot实现在开始考虑优化之前等待那么多方法调用或循环迭代.我还建议在核心测试运行之间进行方法调用,因为虽然热点可以执行"堆栈替换"(OSR),但它在实际应用程序中并不常见,并且它与交换方法的整个实现的行为完全不同.
归档时间: |
|
查看次数: |
314 次 |
最近记录: |