静态常量、静态变量和性能

5

虽然这不是它的主要目的,但我一直认为在某些情况和VM实现中,final关键字可以帮助JIT。
可能这是一个谣言,但我从未想象过将字段设置为final会对性能产生负面影响。

直到我遇到了这样的代码:

   private static final int THRESHOLD = 10_000_000;
   private static int [] myArray = new int [THRESHOLD];

   public static void main(String... args) {
      final long begin = System.currentTimeMillis();

      //Playing with myArray
      int index1,index2;
      for(index1 = THRESHOLD - 1; index1 > 1; index1--)
          myArray[index1] = 42;             //Array initial data
      for(index1 = THRESHOLD - 1; index1 > 1; index1--) {
                                            //Filling the array
          for(index2 = index1 << 1; index2 < THRESHOLD; index2 += index1)
              myArray[index2] += 32;
      }

      long result = 0;
      for(index1 = THRESHOLD - 1; index1 > 1; index1-=100)
          result += myArray[index1];

      //Stop playing, let's see how long it took
      System.out.println(result);
      System.out.println((System.currentTimeMillis())-begin+"ms");
   }

让我们来看一下:private static int [] myArray = new int [THRESHOLD];。在64位W7上,基于10次连续运行的结果,我得到了以下结果:
  1. THRESHOLD = 10^7,1.7.0u09客户端VM(Oracle):

    • myArray不是final时,在约2133毫秒内运行。
    • myArray是final时,在约2287毫秒内运行。
    • -server VM产生类似的数字即2131ms和2284ms。

  2. THRESHOLD = 3x10^7,1.7.0u09客户端VM(Oracle):

    • myArray不是final时,在约7647毫秒内运行。
    • myArray是final时,在约8190毫秒内运行。
    • -server VM产生大约7653ms和8150ms。

  3. THRESHOLD = 3x10^7,1.7.0u01客户端VM(Oracle):

    • myArray不是final时,在约8166毫秒内运行。
    • myArray是final时,在约9694毫秒内运行。这超过了15%的差异!
    • -server VM在非final版本中产生了可以忽略的有利差异,大约为1%。

备注:我使用JDK 1.7.0u09的javac生成的字节码进行了所有测试。除了myArray声明外,两个版本生成的字节码完全相同。

那么为什么带有静态final myArray的版本比带有静态myArray的版本慢?


编辑(使用Aubin版本的我的片段):

似乎带有final关键字和不带有的版本之间的差异仅在于第一个迭代。 不知何故,在第一个迭代中,带有final的版本始终比其没有的版本慢,然后下一次迭代具有类似的时间记录。

例如,对于THRESHOLD = 10^8并在1.7.0u09客户端上运行,第一次计算大约需要35秒,而第二次“仅”需要30秒。

显然,虚拟机执行了一项优化,那是JIT在发挥作用,为什么它没早点触发呢(例如编译嵌套循环的第二层,这部分是热点代码)?请注意,我的评论对于1.7.0u01客户端VM仍然有效。使用该版本(及之前的版本),带有"final myArray"关键字的代码运行速度比不带该关键字的慢:200次迭代基础上,运行时间为2671ms与2331ms之间。

2
只有2秒的运行时间?我不认为JIT真的进行了一些严格的优化。 - Thomas Jungblut
4
10分不够,1000分更好,而第一次得分可能不会被计入。 - Aubin
@Aubin,这需要一些时间,等完成后我会更新帖子。 - Jerome
1
同时从内部循环中删除控制台写入。 - Thomas Jungblut
为了符合该度量标准,最小值、最大值和平均值(至少)应当被公布。 - Aubin
1个回答

4

我认为应该不考虑 System.out.println( result ) 的时间,因为I/O非常耗费时间且变化巨大。

我认为 println() 的影响因素比 final 巨大得多。

我建议将性能测试编写如下:

public class Perf {
   private static final int   THRESHOLD = 10_000_000;
   private static final int[] myArray   = new int[THRESHOLD];
   private static /* */ long  min = Integer.MAX_VALUE;
   private static /* */ long  max = 0L;
   private static /* */ long  sum = 0L;

   private static void perf( int iteration ) {
      final long begin = System.currentTimeMillis();

      int index1, index2;
      for( index1 = THRESHOLD - 1; index1 > 1; index1-- ) {
         myArray[ index1 ] = 42;
      }
      for( index1 = THRESHOLD - 1; index1 > 1; index1-- ) {
         for( index2 = index1 << 1; index2 < THRESHOLD; index2 += index1 ) {
            myArray[ index2 ] += 32;
         }
      }
      long result = 0;
      for( index1 = THRESHOLD - 1; index1 > 1; index1 -= 100 ) {
         result += myArray[ index1 ];
      }
      if( iteration > 0 ) {
         long delta = System.currentTimeMillis() - begin;
         sum += delta;
         min = Math.min(  min,  delta );
         max = Math.max(  max,  delta );
         System.out.println( iteration + ": " + result );
      }
   }

   public static void main( String[] args ) {
      for( int iteration = 0; iteration < 1000; ++iteration ) {
         perf( iteration );
      }
      long average = sum / 999;// the first is ignored
      System.out.println( "Min    : " + min     + " ms" );
      System.out.println( "Average: " + average + " ms" );
      System.out.println( "Max    : " + max     + " ms" );
   }
}

仅进行10次迭代的结果如下:

最终结果为:

Min    : 7645 ms
Average: 7659 ms
Max    : 7926 ms

没有最终版本:

Min    : 7629 ms
Average: 7780 ms
Max    : 7957 ms

我建议读者运行这个测试并发布他们的结果以进行比较。


我实际上感兴趣的是final的影响。关于println(result),我使用它是为了确保result被完全计算(谁知道呢...)。 - Jerome
是的,这是因为你没有测量它。 - Aubin
@Aubin:离题了,"AMUO" 是什么意思? - JB Nizet
通过您的代码,我发现使用final和不使用final得到的时间相似。但是,如果您将perf函数嵌套在main函数内部,则会出现我所描述的行为。我的100次运行结果:2760毫秒与2320毫秒,我使用了不同的JRE的eclipse,因此与我原始帖子中的结果有所偏差。 - Jerome

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接