为什么浮点运算在预热阶段会更快?

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),但它在实际应用程序中并不常见,并且它与交换方法的整个实现的行为完全不同.