简体   繁体   English

从回调类执行代码时,Java性能测试会发生变化。 Java堆栈框架问题?

[英]Java performance testing changes when executing code from callback class. Java stack frame issue?

I was trying to write a fairly generic testing framework for profiling a group of functions, but ran into an issue I couldn't place my finger on detailed below. 我试图编写一个相当通用的测试框架来分析一组函数,但遇到了一个问题,我无法在下面详细说明。

The concept is pretty simple. 这个概念非常简单。 I created a Test abstract class: 我创建了一个Test抽象类:

public abstract class Test {
    private final String name;

    public Test(String name) {
        this.name = name;
    }

    public abstract void test();
}

Then, I have a master test class, with some configuration information and the loops for running the tests. 然后,我有一个主测试类,包含一些配置信息和用于运行测试的循环。

public class MyTestClass {
    public static double staticMethod1(Quat4f q) {
        double angle;

        float dirW = q.w;

        if (q.w * q.w + q.y * q.y > 0.0f) {
            if (q.w > 0.f && q.y < 0.0f)
                dirW *= -1.0f;

            angle = 2.0f * Math.acos(dirW);
        } else {
            angle = 0.0f;
        }

        return angle / 6.283f * 100.f;
    }

    public static double staticMethod2(Quat4f q) {
        AxisAngle4f axisAngle = new AxisAngle4f();
        axisAngle.set(q);
        return axisAngle.angle / 6.283f * 100.f;
    }

    public static final void main(String[] args) {
        final Quat4f quaternion = new Quat4f(0, 0, 0, 1);

        Test[] tests = new Test[] {
            new Test("staticMethod1") {
                @Override
                public void test() {
                    staticMethod1(quaternion);
                }
            },
            new Test("staticMethod2") {
                @Override
                public void test() {
                    staticMethod2(quaternion);
                }
            }
        };

        long startTime = 0;
        int repeat = 10; //How many times to repeat each iteration.
        int[] tiers = new int[] { 1000, 10000, 100000, 1000000 };
        long[][][] times = new long[tests.length][tiers.length][iterations];

        for (int testIndex = 0; testIndex < tests.length; testIndex++) {
            for (int tierIndex = 0; tierIndex < tiers.length; tierIndex++) {
                for (int r = 0; r < repeat; r++) {
                    startTime = System.nanoTime();

                    for (int i = 0; i < tiers[tierIndex]; i++) {
                        tests[testIndex].run(); //run the test
                    }

                    times[testIndex][tierIndex][r] = System.nanoTime() - startTime; //Stash the execution time in the array.
                }
            }
        }
    }
}

Looking at this code, you are probably wondering why the Test.run() method calls a static method. 查看此代码,您可能想知道为什么Test.run()方法调用静态方法。 That's because originally I was just embedding the hardcoded call to the static method inside of the loop. 那是因为最初我只是将硬编码调用嵌入到循环内部的静态方法中。 Instead of calling tests[testIndex].run() I would call staticMethod1(quaternion) explicitly there. 我没有调用tests[testIndex].run()而是在那里显式调用staticMethod1(quaternion) That also meant redundantly duplicating this loop code for each method - which led me to creating the abstract base class so that I could essentially create an array of callbacks to test. 这也意味着为每个方法冗余地复制这个循环代码 - 这使我创建了抽象基类,这样我就可以创建一个回调数组来测试。

Observations 意见

So, after switching to using the callback Test class, instead of calling the static function explicitly, I observed two things: 因此,在切换到使用回调Test类之后,我没有明确地调用静态函数,而是观察了两件事:

  • The execution times of each call increased greatly 每次通话的执行时间大大增加
  • The execution times were not nearly as consistent. 执行时间几乎不一致。

Static Results 静态结果

Here is a table of output from when I was calling the static methods explicitly (all times in nano seconds): 这是我明确调用静态方法时的输出表(所有时间都以纳秒为单位):

1000    10000   100000  1000000 
staticMethod1
315358  424208  1451141 14495864    
125334  410525  1483797 14657896    
125956  412079  1445543 14702681    
150837  413012  1473845 14677179    
126578  412080  1419419 14415313    
126268  413323  1450830 14600361    
125645  411457  1437147 14504261    
126889  414257  1431548 14402563    
129689  420476  1476954 14548734    
848417  425453  1471046 14409715    

staticMethod2
1528581 1031287 3137712 2180755 
228899  303540  218947  2227406 
228276  303228  218324  2232071 
235118  301362  218324  2174224 
226411  317534  218636  2180133 
226410  302918  218946  2143434 
253779  302606  219879  2116689 
226099  349257  219880  2108913 
240717  303228  218946  2150899 
250358  303228  218946  2159607 

Callback Results 回调结果

And here is the same code, only executed from my abstract base Test class: 这里是相同的代码,只从我的抽象基类Test类执行:

1000    10000   100000  1000000 
staticMethod1
360453  454686  1985445 15699447    
155191  449400  1639298 15048205    
152391  449089  1576165 15128134    
175095  451888  1537289 15300429    
156746  466816  1600734 15190645    
157989  464950  1641476 15483610    
157368  452198  1559681 15316290    
157990  460285  1572122 15402439    
157367  527773  1538222 15078995    
878274  454065  1548485 15077439    

staticMethod2
1519562 1101263 1674130 8842756 
274616  335883  1481309 8728930 
285190  339616  1471046 8842135 
291721  334950  1280089 8591155 
294831  347391  1339491 13402065    
332152  343970  1299683 10950426    
300429  326553  1252100 7778814 
285190  324999  1365615 8569385 
297008  341792  1284133 7734030 
283324  326554  1327984 11505256

Question Revisited 问题重新审视

The only conjecture I was able to form was that perhaps this had something to do with the Java stack frame concept. 我能够形成的唯一猜想是,这可能与Java堆栈框架概念有关。 I'm really looking for someone to give an in depth analysis of why this would happen. 我真的在寻找能够深入分析为什么会发生这种情况的人。

What could be the result of these dramatically different results? 这些显着不同的结果可能是什么结果?


I re-ran the tests from my home computer, so I thought I would re-post those results for accuracy. 我从家用电脑上重新测试了测试,所以我想我会重新发布这些测试结果以保证准确性。

Callback (staticMethod1, staticMethod2) 回调(staticMethod1,staticMethod2)

1000    10000   100000  1000000 
staticMethod1
629020  688864  3016204 40796517    
348542  589891  2662401 39673949    
355447  611921  3559403 39613447    
416936  617511  4022701 39335929    
412660  635267  4290355 38862108    
409702  751996  4055583 38823967    
405426  761202  4063803 38998238    
410030  760545  4024016 39131407    
411346  656640  3877366 38737489    
1794991 723060  4139759 38286028    

staticMethod2
2219818 4198617 2526272 15647236    
735555  1939011 2651879 14482251    
761860  445542  2480238 12990096    
734569  222607  2437822 14278058    
734898  264366  2323394 23561771    
743118  220633  2739672 15669266    
746734  224909  5159080 12916113    
781918  223593  2342794 14843616    
789481  229512  2740658 13400784    
865108  227210  5202155 22015033

Callback (staticMethod2, staticMethod1) 回调(staticMethod2,staticMethod1)

1000    10000   100000  1000000 
staticMethod2
2159974 1598690 4343951 4011522 
755284  484013  4646131 3989491 
779945  460667  390302  4114111 
866752  469874  413318  3833963 
911141  495193  433376  4024016 
878918  468230  424827  4162118 
892070  452447  431074  3830346 
806579  419894  463298  4003301 
814142  424169  424826  3961871 
830253  417593  432718  3823112 

staticMethod1
768437  632637  4596480 38401771    
421539  655325  3811603 37536663    
418579  657626  3917481 37377517    
425813  648091  3887230 37347924    
423512  653023  3800095 38334692    
428772  570820  3810288 37640568    
435020  581013  3795162 37543896    
426800  578382  3805027 37390670    
448830  567861  4004617 37502466    
1883443 663874  3848101 38096961    

Callback (staticMethod1, staticMethod2) with single instantiation of AxisAngle4f 具有AxisAngle4f单实例化的回调(staticMethod1,staticMethod2)

1000    10000   100000  1000000 
staticMethod1
693138  745420  4382752 26091003    
405098  677355  3378227 41866476    
390630  669793  4349213 42472807    
430088  699057  4296931 27899147    
385697  675711  4300549 42643790    
382410  658941  4296603 32330563    
393918  662888  2602557 42622417    
380437  666833  2588747 32903026    
393918  738515  2616367 26079823    
1805843 679985  2570004 42191343    

staticMethod2
444556  1640449 963422  8620168 
463298  464942  946325  8545856 
431732  474478  877931  8645487 
452776  466915  870698  8761229 
432718  449487  882534  8572490 
443898  464613  876288  8482066 
414633  538596  871684  8672121 
408715  190054  876287  8626744 
405427  96342   874643  8607016 
436664  96343   847681  8543883 

These results aren't really "dramatic." 这些结果并非真正“戏剧化”。 The averages for the 1000000 runs are: 1000000次运行的平均值为:

  • staticMethod1 staticMethod1
    • explicit: 14541456.7 ( 14.5 ns per iteration) 显式:14541456.7(每次迭代14.5 ns
    • callback: 15272563.3 ( 15.2 ns per iteration) 回调:15272563.3(每次迭代15.2 ns
    • slowdown: 0.7 ns per iteration 减速: 每次迭代0.7 ns
  • staticMethod2 staticMethod2
    • explicit: 2167413.1 ( 2.2 ns per iteration) 显式:2167413.1(每次迭代2.2 ns
    • callback: 9494495.2 ( 9.5 ns per iteration) 回调:9494495.2(每次迭代9.5 ns
    • slowdown: ( 7 ns per iteration ) 减速:( 每次迭代7 ns

The fact that the slowdown between these is off by (almost exactly) an order of magnitude is weird, but the big thing is that the callback approach slowed down by at most about 7 ns. 它们之间的减速(几乎完全)偏离一个数量级这一事实很奇怪,但最重要的是回调方法最多减慢了约7 ns。 That's not a whole lot. 那不是很多。

UPDATE UPDATE

More results you have posted, with the order of testing method2 then method1 , are consistent with the following: 更多结果已公布,与测试顺序method2method1 ,与以下一致:

  1. the JVM initially runs your code in interpreted mode, profiling it; JVM最初以解释模式运行您的代码,对其进行分析;
  2. it sees that you always call the same implmentation of the Test class; 它看到你总是调用Test类的相同实现;
  3. so it compiles that call site into a monomorphic call site; 所以它将该呼叫站点编译成一个单形呼叫站点;
  4. later, the call changes to a different class. 之后,调用将更改为其他类。 The monomorphic assertion fails, causing immediate recompilation to a megamorphic call site. 单态断言失败,导致立即重新编译到一个变形调用站点。 There is no profiling going on in JIT-compiled code, so there will be no revisiting of that call site to find out it could be once again recompiled into a monomorphic site specialized for the new case. 在JIT编译的代码中没有进行分析,因此不会重新访问该调用站点以发现它可以再次重新编译成专用于新案例的单形站点。

The speed advantage of a monomorphic site is both in avoiding virtual function table lookup, and in allowing method inlining to happen. 单态站点的速度优势在于避免虚函数表查找,以及允许方法内联发生。 Both your methods are quite short and good candidates for inlining. 你的方法都很短,很适合内联。

You could further verify that such recompiling happens once, but only once, by using --XX:+PrintCompilation combined with a println at the moment you switch from one test to the other. 您可以通过在从一个测试切换到另一个测试时使用--XX:+PrintCompilationprintln结合来进一步验证这种重新编译只发生一次,但只发生一次。

Further notes 进一步说明

  1. You should avoid your timings to be influenced by such intricacies of the JVM. 您应该避免您的时间受到JVM的这些复杂性的影响。 To that end, use a test(int iterations) method and push your innermost loop into it. 为此,使用test(int iterations)方法并将最内层的循环推入其中。 This will allow the critical method dispatch to happen only once per at least 1,000 iterations, and become insignificant; 这将允许关键方法调度每次至少1000次迭代仅发生一次,并且变得无关紧要;

  2. mistrust any time measurement below 1 ms. 不信任任何时间测量低于1毫秒。 System.nanoTime 's accuracy is nowhere close to 1 ns: its granularity is usually around 1 µs, and to get good accuracy you must be well above that. System.nanoTime的精度远不及1 ns:它的粒度通常约为1μs,为了获得良好的精度,你必须远高于此。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM