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

Posted

技术标签:

【中文标题】等效静态和非静态方法的速度差异很大【英文标题】: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 可以在这里启发我们) 【参考方案1】:

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

调查:

    使用-XX:+UnlockDiagnosticVMOptions -XX:+PrintCompilation -XX:+PrintInlining VM 选项运行 java 表明这两种方法都已编译和内联 查看生成的程序集的方法本身没有显着差异 然而,一旦它们被内联,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,每次运行都不同

以上是关于等效静态和非静态方法的速度差异很大的主要内容,如果未能解决你的问题,请参考以下文章

PHP代码优化

Php优化方案

PHP 代码优化建议

静态类和非静态类方法

PHP高效率写法

关于静态方法和非静态方法