Java:为什么计算速度比赋值(int)快?

3

以下是同一函数的两个版本(基本上是通过暴力破解尝试恢复密码),它们的性能不同:

第一版:

private static final char[] CHARS = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789".toCharArray();
private static final int N_CHARS = CHARS.length;
private static final int MAX_LENGTH = 8;

private static char[] recoverPassword()
{
   char word[];
   int refi, i, indexes[];

   for (int length = 1; length <= MAX_LENGTH; length++)
   {
      refi = length - 1;
      word = new char[length];
      indexes = new int[length];
      indexes[length - 1] = 1;

      while(true)
      {
         i = length - 1;
         while ((++indexes[i]) == N_CHARS)
         {
            word[i] = CHARS[indexes[i] = 0];
            if (--i < 0)
               break;
         }

         if (i < 0)
            break;

         word[i] = CHARS[indexes[i]];

         if (isValid(word))
            return word;
      }
   }
   return null;
}

Version 2:

private static final char[] CHARS = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789".toCharArray();
private static final int N_CHARS = CHARS.length;
private static final int MAX_LENGTH = 8;

private static char[] recoverPassword()
{
   char word[];
   int refi, i, indexes[];

   for (int length = 1; length <= MAX_LENGTH; length++)
   {
      refi = length - 1;
      word = new char[length];
      indexes = new int[length];
      indexes[length - 1] = 1;

      while(true)
      {
         i = refi;
         while ((++indexes[i]) == N_CHARS)
         {
            word[i] = CHARS[indexes[i] = 0];
            if (--i < 0)
               break;
         }

         if (i < 0)
            break;

         word[i] = CHARS[indexes[i]];

         if (isValid(word))
            return word;
      }
   }
   return null;
}

我预期版本2会更快,因为它有以下改进(这也是唯一的区别):

i = refi;

相较于版本1,有以下改进:

i = length -1;

然而,事实恰好相反:版本1的速度快了3%以上! 有人知道为什么吗?这是编译器进行了一些优化吗?

非常感谢大家迄今为止的答案。只是想补充一下,实际目标不是优化这段代码(已经相当优化了),而更多地从编译器 / CPU / 架构的角度理解,什么可能会解释这样的性能差异。 这些答案非常有帮助,再次感谢!

关键字


7
请确保您的基准测试正确。对Java进行基准测试并不总是容易的,而3%并不算多。 - Jon Kiparsky
4个回答

5

在微基准测试中,很难检查这一点,因为您无法确定代码已经如何进行优化而不阅读生成的机器代码,即使CPU可以执行许多技巧来优化其未来,例如它将x86代码转换为RISC风格指令以实际执行。

计算最少需要一个周期,CPU可以同时执行三个计算。访问L1缓存需要4个周期,访问L2、L3、主内存分别需要11、40-75、200个周期。

在许多情况下,为了避免简单计算而存储值实际上会更慢。顺便说一句,使用除法和取模操作非常昂贵,缓存此值在微调代码时可能是值得的。


1
正确答案应该能够通过逆汇编器(即 .class -> .java 转换器)检索到, 但我的猜测是编译器可能已经决定彻底摆脱 iref 并决定将 length - 1 存储在辅助寄存器中。 我更倾向于使用 c++,但我的建议是尝试:
const int refi = length - 1;

在for循环内部。此外,你可能应该使用。
indexes[ refi ] = 1;

1
在Java中的等价写法是 final int refi = length - 1 - Jason C
不会的;但是@Parakram从未建议他查看类文件,而是建议使用反编译器来查看编译器从原始源文件中优化了什么。 - Jurgen Camilleri
@JurgenCamilleri 一个反编译器可以提供一些信息,但是有些字节码在Java中没有等效的表示方式,例如,反编译器无法显示switch块是编译为跳转表还是一系列isub+ifeq或偏移*值。 - Jason C

1

比较代码运行时间并不能给出精确或隔离的结果

首先,这不是比较性能的正确方式。需要进行运行时间分析。两个代码具有相同的循环结构,它们的运行时间相同。当您运行代码时,可能会有不同的运行时间。然而,它们大多数情况下与缓存命中、I/O时间、线程和进程调度有关。没有确切的保证代码总是在精确的时间内完成。

然而,您的代码仍然存在差异,要了解差异,您应该查看CPU架构。我可以根据x86架构进行基本解释。

幕后发生了什么?

i = refi;

CPU 从 RAM 中取回 refii 的值存储到寄存器中。如果这些值不在缓存中,则会对 RAM 进行两次访问,而且 i 的值将被写入 RAM。然而,根据线程和进程的调度情况,每次操作花费的时间总是不同的。此外,如果这些值在虚拟内存中,则需要更长的时间。

i = length -1;

CPU也可以从RAM或高速缓存中访问ilength,访问次数相同。此外,这里还有一个减法,意味着额外的CPU周期。这就是为什么你认为这个需要更长时间来完成。这是预期的,但我上面提到的问题解释了为什么需要更长时间。

总结

正如我所解释的,这不是比较性能的方式。我认为,这些代码之间没有真正的区别。CPU内部和编译器中有很多优化。如果您反编译.class文件,可以看到优化后的代码。

我的建议是尽量减少BigO运行时间分析。如果您找到更好的算法,那是优化代码的最佳方法。如果您的代码仍然存在瓶颈,可以尝试微基准测试。

另请参阅

算法分析

大O符号

微处理器

编译器优化

CPU调度


0
首先,你不能通过运行程序来比较性能 - Java中的微基准测试是复杂的
另外,现代CPU上的减法平均只需要三分之一的时钟周期。在一个3GHz的CPU上,这是0.1纳秒。而且没有任何东西可以告诉你减法实际上发生了,因为编译器可能已经修改了代码。
所以:
  • 你应该尝试检查生成的汇编代码
  • 如果你真的关心性能,就要创建一个适当的微基准测试。

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