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

时间:2014-08-30 14:16:01

标签: java performance optimization floating-point

我最初想用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();
    }
}

结果没有预热阶段:

Divide by 5.0f: 382224
Multiply with 0.2f: 490765

结果 预热阶段:

Divide by 5.0f: 22081
Multiply with 0.2f: 10885

另一个我无法解释的有趣变化是什么操作变得更快(除法与乘法)。正如前面提到的那样,如果没有热身赛,赛区的速度似乎有点快,而热身赛的速度似乎要慢两倍。

我尝试添加一个初始化块,将值设置为随机值,但它不会影响结果,也没有添加多个预热阶段。方法运作的数字是相同的,所以这不是原因。

这种行为的原因是什么?什么是这个预热阶段以及它如何影响性能,为什么在预热阶段操作会更快,为什么操作更快会出现转机?

2 个答案:

答案 0 :(得分:12)

在热身之前,Java将通过解释器运行字节码,想一想如何编写一个可以在java中执行java字节码的程序。预热后,热点将为您正在运行的cpu生成本机汇编程序;利用该cpus功能集。两者之间存在显着的性能差异,解释器将为单字节代码运行许多cpu指令,其中热点生成本机汇编代码,就像gcc在编译C代码时所做的那样。这是除法和乘法的时间之间的差异最终会降低到运行的CPU,而且它只是一个cpu指令。

问题的第二部分是热点还记录了衡量代码运行时行为的统计信息,当它决定优化代码时,它将使用这些统计信息来执行在编译时不一定可能的优化。例如,它可以降低空值检查,分支错误预测和多态方法调用的成本。

简而言之,必须放弃预热后的结果。

Brian Goetz在这个主题上写了一篇非常好的文章here

======

附录:概述了什么&#39; JVM热身&#39;装置

JVM&#39;热身&#39;是一个松散的短语,并不再严格地说是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有机会收集统计信息并优化&#39 ;热点地区&#39;的代码。 10k是一个神奇的数字,因为Server Hotspot实现在开始考虑优化之前等待那么多方法调用或循环迭代。我还建议在核心测试运行之间进行方法调用,因为热点可以在堆栈替换时进行。 (OSR),它在实际应用程序中并不常见,并且与交换方法的整个实现的行为完全不同。

答案 1 :(得分:4)

你没有测量任何有用的东西&#34;没有预热阶段&#34 ;;您是否正在测量解释代码的速度,以及生成堆栈内替换所需的时间。也许分裂导致编译更早开始。

有许多指南和各种软件包可用于构建不会受到这些问题影响的微基准测试。如果您打算继续做这类事情,我建议您阅读指南并使用现成的软件包。