等效静态方法和非静态方法的速度差异很大


86

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

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。*似乎可以始终如一地再现原始结果。


4
您如何运行基准测试?我敢打赌这是JVM的产物,没有足够的时间来优化代码。
帕特里克·柯林斯

2
似乎它的足够的时间让JVM来编译和执行的主要方法为OSR+PrintCompilation +PrintInlining节目
Tagir Valeev

1
我已经尝试了代码片段,但是没有得到Stabbz所说的任何时差。它们为56282ms(使用实例)为54551ms(作为静态方法)。
唐查卡潘

1
@PatrickCollins必须有五秒钟的时间。我重新编写了一下,以便您可以同时测量这两个值(每个变体启动一个JVM)。我知道作为基准它仍然存在缺陷,但足以令人信服:STATIC为1457毫秒,NON_STATIC为5312毫秒。
maaartinus

1
尚未详细调查问题,但这可能与以下问题有关:shipilev.net/blog/2015/black-magic-method-dispatch(也许AlekseyShipilëv可以在这里启发我们)
Marco13 2015年

Answers:


72

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

调查:

  1. 使用-XX:+UnlockDiagnosticVMOptions -XX:+PrintCompilation -XX:+PrintInliningVM选项运行Java会显示两种方法均已编译和内联
  2. 查看生成的程序集本身的方法没有显着差异
  3. 但是,一旦它们内联,内部生成的程序集main就会非常不同,实例方法会更加积极地进行优化,尤其是在循环展开方面

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

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

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


在52到71之间,恢复了原始行为(至少在我的机器上,例如我的答案)。看起来静态版本要大20个单位,但是为什么呢?这很奇怪。
maaartinus

3
@maaartinus我什至不知道该数字确切代表什么-该文档相当容易规避:“具有服务器编译器中间表示节点的展开循环主体的计数小于该值。服务器编译器使用的限制是该值的函数,而不是实际值。默认值随运行JVM的平台而变化。 “ ...
assylias,2015年

我都不知道,但是我的第一个猜测是,静态方法在任何单位上都会变得略大,而我们找到了最重要的地方。但是,差异非常大,所以我目前的猜测是静态版本得到了一些优化,使其变得更大。我没有看过生成的asm。
maaartinus

33

只是未经证实的猜测基于亚述的答案。

JVM使用一个阈值来进行循环展开,该阈值大约为70。无论出于何种原因,静态调用都会更大,并且不会展开。

更新结果

  • 随着LoopUnrollLimit在低于52,这两个版本是缓慢的。
  • 在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

对于那些愿意尝试的人,我的版本可能会有用。


是1456毫秒的时间吗?如果是,为什么您说静态速度慢?
托尼

@Tony我NON_STATIC和混淆了STATIC,但是我的结论是正确的。现在已修复,谢谢。
maaartinus

0

在调试模式下执行此操作时,实例和静态用例的编号相同。这进一步意味着,JIT在静态情况下犹豫将代码编译为本机代码,方法与在实例方法情况下一样。

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


“为什么这样做?很难说,如果这是一个更大的应用程序,那么它可能会做正确的事情。” 否则,您可能会遇到一个奇怪的性能问题,该问题太大而无法实际调试。(这并不难说。您可以像sysylias一样查看JVM吐出的程序集。)
tmyklebu 2015年

@tmyklebu否则,我们会遇到一个怪异的性能问题,即进行完全调试既不必要又昂贵,并且有一些简单的解决方法。最后,我们在这里谈论JIT,它的作者并不知道JIT在所有情况下的行为。:)查看其他答案,它们非常好,非常接近,可以解释问题,但到目前为止,仍然没人知道这到底是为什么发生的。
Dragan Bozanovic 2015年

@DraganBozanovic:当它在实际代码中引起真正的问题时,它不再是“不必要的完全调试”。
tmyklebu 2015年

0

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

输出:

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:何时使用静态方法


“您应该凭经验使用静态和动态方法。” 这条经验法则是什么?你是谁/什么?
weston,2015年

@weston对不起,我没有添加我正在考虑的链接:)。thx
nafas 2015年

0

请尝试:

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毫秒至23000+毫秒,每次运行都不同
Stabbz
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.