【问题标题】:Large difference in speed of equivalent static and non static methods等效静态和非静态方法的速度差异很大
【发布时间】:2015-08-07 21:53:38
【问题描述】:

在这段代码中,当我在 main 方法中创建一个对象然后调用该对象方法:ff.twentyDivCount(i)(runs in 16010 ms) 时,它的运行速度比使用此注释调用它快得多:twentyDivCount(i)(runs 59516 毫秒)。当然,当我在不创建对象的情况下运行它时,我将方法设为静态,因此可以在 main 中调用它。

public class ProblemFive {

    // Counts the number of numbers that the entry is evenly divisible by, as max is 20
    int twentyDivCount(int a) {    // Change to static int.... when using it directly
        int count = 0;
        for (int i = 1; i<21; i++) {

            if (a % i == 0) {
                count++;
            }
        }
        return count;
    }

    public static void main(String[] args) {
        long startT = System.currentTimeMillis();;
        int start = 500000000;
        int result = start;

        ProblemFive ff = new ProblemFive();

        for (int i = start; i > 0; i--) {

            int temp = ff.twentyDivCount(i); // Faster way
                       // twentyDivCount(i) - slower

            if (temp == 20) {
                result = i;
                System.out.println(result);
            }
        }

        System.out.println(result);

        long end = System.currentTimeMillis();;
        System.out.println((end - startT) + " ms");
    }
}

编辑:到目前为止,似乎不同的机器会产生不同的结果,但使用 JRE 1.8.* 似乎可以始终如一地重现原始结果。

【问题讨论】:

  • 你是如何运行你的基准测试的?我敢打赌,这是 JVM 没有足够时间优化代码的产物。
  • 看来 JVM 已经有足够的时间来编译并为 main 方法执行 OSR,如+PrintCompilation +PrintInlining 所示
  • 我已经尝试过代码 sn-p ,但我没有像 Stabbz 所说的那样得到任何时差。他们56282ms(使用实例)54551ms(作为静态方法)。
  • @PatrickCollins 五秒钟就足够了。我rewrote it a bit 以便您可以测量两者(每个变体启动一个 JVM)。我知道作为基准它仍然存在缺陷,但它足以令人信服:1457 ms STATIC vs 5312 ms NON_STATIC。
  • 还没有详细调查这个问题,但是这个可能是相关的:shipilev.net/blog/2015/black-magic-method-dispatch(也许 Aleksey Shipilëv 可以在这里启发我们)

标签: java performance object methods static


【解决方案1】:

使用 JRE 1.8.0_45 我得到了类似的结果。

调查:

  1. 使用-XX:+UnlockDiagnosticVMOptions -XX:+PrintCompilation -XX:+PrintInlining VM 选项运行 java 表明这两种方法都已编译和内联
  2. 查看生成的程序集的方法本身没有显着差异
  3. 然而,一旦它们被内联,main 中生成的程序集就大不相同了,实例方法得到了更积极的优化,尤其是在循环展开方面

然后我再次运行您的测试,但使用不同的循环展开设置来确认上述怀疑。我运行了你的代码:

  • -XX:LoopUnrollLimit=0 并且这两个方法运行缓慢(类似于带有默认选项的静态方法)。
  • -XX:LoopUnrollLimit=100 并且两种方法都运行得很快(类似于带有默认选项的实例方法)。

作为一个结论,似乎在默认设置下,热点 1.8.0_45 的 JIT 在方法为静态时无法展开循环(尽管我不确定为什么会这样行为方式)。其他 JVM 可能会产生不同的结果。

【讨论】:

  • 在 52 和 71 之间,恢复了原始行为(至少在我的机器上,我的回答)。看起来静态版本要大 20 个单位,但为什么呢?这很奇怪。
  • @maaartinus 我什至不确定这个数字究竟代表什么 - 文档相当回避:“Unroll loop body with server compiler intermediate representation node count less than this value. 使用的限制服务器编译器是这个值的函数,而不是实际值。默认值随JVM运行的平台而异。"...
  • 我也不知道,但我的第一个猜测是静态方法在任何单位中都会变得稍大一些,并且我们找到了重要的地方。然而,差异是相当大的,所以我目前的猜测是静态版本得到了一些优化,使其更大一些。我没有看过生成的asm。
【解决方案2】:

在调试模式下执行时,实例和静态情况的数字相同。这进一步意味着 JIT 在静态情况下会犹豫将代码编译为本机代码,就像在实例方法情况下一样。

为什么会这样?这很难说;如果这是一个更大的应用程序,它可能会做正确的事情......

【讨论】:

  • “为什么要这样做?很难说,如果这是一个更大的应用程序,它可能会做正确的事情。”或者你只是有一个奇怪的性能问题,它太大而无法实际调试。 (这并不难说。你可以看看 JVM 像 assylias 那样吐出的程序集。)
  • @tmyklebu 或者我们有一个奇怪的性能问题,完全调试是不必要且昂贵的,并且有简单的解决方法。最后,我们在这里讨论的是 JIT,它的作者并不知道它在所有情况下的确切行为。 :) 看看其他答案,它们非常好并且非常接近地解释了这个问题,但到目前为止,仍然没有人知道为什么会发生这种情况。
  • @DraganBozanovic:当它在实际代码中引起真正的问题时,它不再是“不必要的完全调试”。
【解决方案3】:

我只是稍微调整了一下测试,得到了以下结果:

输出:

Dynamic Test:
465585120
232792560
232792560
51350 ms
Static Test:
465585120
232792560
232792560
52062 ms

注意

当我单独测试它们时,我得到了大约 52 秒的动态和大约 200 秒的静态。

这是程序:

public class ProblemFive {

    // Counts the number of numbers that the entry is evenly divisible by, as max is 20
    int twentyDivCount(int a) {  // Change to static int.... when using it directly
        int count = 0;
        for (int i = 1; i<21; i++) {

            if (a % i == 0) {
                count++;
            }
        }
        return count;
    }

    static int twentyDivCount2(int a) {
         int count = 0;
         for (int i = 1; i<21; i++) {

             if (a % i == 0) {
                 count++;
             }
         }
         return count;
    }

    public static void main(String[] args) {
        System.out.println("Dynamic Test: " );
        dynamicTest();
        System.out.println("Static Test: " );
        staticTest();
    }

    private static void staticTest() {
        long startT = System.currentTimeMillis();;
        int start = 500000000;
        int result = start;

        for (int i = start; i > 0; i--) {

            int temp = twentyDivCount2(i);

            if (temp == 20) {
                result = i;
                System.out.println(result);
            }
        }

        System.out.println(result);

        long end = System.currentTimeMillis();;
        System.out.println((end - startT) + " ms");
    }

    private static void dynamicTest() {
        long startT = System.currentTimeMillis();;
        int start = 500000000;
        int result = start;

        ProblemFive ff = new ProblemFive();

        for (int i = start; i > 0; i--) {

            int temp = ff.twentyDivCount(i); // Faster way

            if (temp == 20) {
                result = i;
                System.out.println(result);
            }
        }

        System.out.println(result);

        long end = System.currentTimeMillis();;
        System.out.println((end - startT) + " ms");
    }
}

我还把测试的顺序改为:

public static void main(String[] args) {
    System.out.println("Static Test: " );
    staticTest();
    System.out.println("Dynamic Test: " );
    dynamicTest();
}

我得到了这个:

Static Test:
465585120
232792560
232792560
188945 ms
Dynamic Test:
465585120
232792560
232792560
50106 ms

如您所见,如果在静态之前调用动态,则静态的速度会大大降低。

基于此基准:

假设这一切都取决于 JVM 优化。因此我 只是建议您按照经验法则使用静态和 动态方法。

经验法则:

Java: when to use static methods

【讨论】:

  • “你要遵循经验法则来使用静态和动态方法。”这个经验法则是什么?你引用的是谁/什么?
  • @weston 抱歉,我没有添加我想要的链接:)。谢谢
【解决方案4】:

只是一个未经证实的猜测,基于 assylias 的回答。

JVM 使用一个阈值来展开循环,大约是 70。无论出于何种原因,静态调用稍大一些,并且不会展开。

更新结果

  • 在下面的 52 中使用 LoopUnrollLimit,这两个版本都很慢。
  • 52到71之间,只有静态版本比较慢。
  • 71 以上,两个版本都很快。

这很奇怪,因为我的猜测是静态调用在内部表示中稍大一些,并且 OP 遇到了一个奇怪的情况。但是好像相差20左右,没有意义。

 

-XX:LoopUnrollLimit=51
5400 ms NON_STATIC
5310 ms STATIC
-XX:LoopUnrollLimit=52
1456 ms NON_STATIC
5305 ms STATIC
-XX:LoopUnrollLimit=71
1459 ms NON_STATIC
5309 ms STATIC
-XX:LoopUnrollLimit=72
1457 ms NON_STATIC
1488 ms STATIC

对于那些愿意尝试的人,my version 可能会有用。

【讨论】:

  • 是 '1456 ms' 时间吗?如果是,为什么你说静态很慢?
  • @Tony 我混淆了NON_STATICSTATIC,但我的结论是正确的。现已修复,谢谢。
【解决方案5】:

请尝试:

public class ProblemFive {
    public static ProblemFive PROBLEM_FIVE = new ProblemFive();

    public static void main(String[] args) {
        long startT = System.currentTimeMillis();
        int start = 500000000;
        int result = start;


        for (int i = start; i > 0; i--) {
            int temp = PROBLEM_FIVE.twentyDivCount(i); // faster way
            // twentyDivCount(i) - slower

            if (temp == 20) {
                result = i;
                System.out.println(result);
                System.out.println((System.currentTimeMillis() - startT) + " ms");
            }
        }

        System.out.println(result);

        long end = System.currentTimeMillis();
        System.out.println((end - startT) + " ms");
    }

    int twentyDivCount(int a) {  // change to static int.... when using it directly
        int count = 0;
        for (int i = 1; i < 21; i++) {

            if (a % i == 0) {
                count++;
            }
        }
        return count;
    }
}

【讨论】:

  • 20273 ms 到 23000+ ms,每次运行都不同
猜你喜欢
  • 2012-04-19
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2020-12-27
  • 1970-01-01
  • 1970-01-01
  • 2016-08-07
  • 2017-03-02
相关资源
最近更新 更多