等效静态法和非静态法在速度上存在较大差异

在这段代码中,当我在 main方法中创建一个 Object,然后调用该对象方法: ff.twentyDivCount(i)(运行时间为16010ms)时,它的运行速度远远快于使用这个注释: twentyDivCount(i)(运行时间为59516ms)。当然,在不创建对象的情况下运行它时,我使方法是静态的,因此可以在 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。* 是原始结果似乎一直被复制的地方。

8715 次浏览

Using JRE 1.8.0_45 I get similar results.

Investigation:

  1. running java with the -XX:+UnlockDiagnosticVMOptions -XX:+PrintCompilation -XX:+PrintInlining VM options shows that both methods get compiled and inlined
  2. Looking at the generated assembly for the methods themselves shows no significant difference
  3. Once they get inlined, however, the generated assembly within main is very different, with the instance method being more aggressively optimised, especially in terms of loop unrolling

I then ran your test again but with different loop unrolling settings to confirm the suspicion above. I ran your code with:

  • -XX:LoopUnrollLimit=0 and both methods run slowly (similar to the static method with the default options).
  • -XX:LoopUnrollLimit=100 and both methods run fast (similar to the instance method with the default options).

As a conclusion it seems that, with the default settings, the JIT of hotspot 1.8.0_45 is not able to unroll the loop when the method is static (although I'm not sure why it behaves that way). Other JVMs may yield different results.

When this is executed in debug mode, the numbers are the same for the instance and static cases. That further means that the JIT hesitates to compile the code to native code in the static case the same way as it does in the instance method case.

Why does it do so? It is hard to say; probably it would do the right thing if this were a larger application...

I just tweaked the test slightly and I got the following results:

Output:

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

NOTE

While I was testing them separately I got ~52 sec for dynamic and ~200 sec for static.

This is the program:

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");
}
}

I also changed the order of the test to:

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

And I got this:

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

As you see, if dynamic is called before static the speed for static dramatically decreased.

Based on this benchmark:

I hypothesize that it all depends on the JVM optimization. thus I just recommend you to go with the rule of thumb for use of static and dynamic methods.

RULE OF THUMB:

Java: when to use static methods

Just an unproved guess based an assylias' answer.

The JVM uses a threshold for loop unrolling, which is something like 70. For whatever reason, the static call is slightly bigger and doesn't get unrolled.

Update results

  • With the LoopUnrollLimit in the below 52, both versions are slow.
  • Between 52 and 71, only the static version is slow.
  • Above 71, both versions are fast.

This is strange as my guess was that the static call is just slightly bigger in the internal representation and the OP hit a strange case. But the difference seems to be about 20, which makes no sense.

 

-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

For those willing to experiment, my version may be useful.

Please try:

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;
}
}