【问题标题】:Why does reversing a loop make it slower?为什么反转循环会使它变慢?
【发布时间】:2015-12-03 03:58:51
【问题描述】:

我有以下代码对数组中的位进行循环移位:

private static void method1(byte[] bytes) {
    byte previousByte = bytes[0];
    bytes[0] = (byte) (((bytes[0] & 0xff) >> 1) | ((bytes[bytes.length - 1] & 0xff) << 7));
    for (int i = 1; i < bytes.length; i++) {
       byte tmp = bytes[i];
       bytes[i] = (byte) (((bytes[i] & 0xff) >> 1) | ((previousByte & 0xff) << 7));
       previousByte = tmp;
    }
}

然后我认为像这样倒退更容易,更易读:

private static void method2(byte[] bytes) {
    byte lastByte = bytes[bytes.length-1];
    for (int i = bytes.length-1; i > 0; i--) {
       bytes[i] = (byte) (((bytes[i] & 0xff) >> 1) | ((bytes[i-1] & 0xff) << 7));
    }
    bytes[0] = (byte) (((bytes[0] & 0xff) >> 1) | ((lastByte & 0xff) << 7));
}

但我注意到第二个(方法 2)比第一个(方法 1)慢!我注意到了差异,因为我调用了该方法数千次。所以我做了一个测试来确定,这里是调用每个方法 3000 次的 20 次测试的平均结果(字节数是 100 万):

method1 average : 4s 572ms
method2 average : 5s 630ms

所以我的问题是:为什么第一个比第二个快?

这是测试代码,以确保我的测试没有做错:

import java.math.BigInteger;

public class BitShiftTests {

public static void main(String[] args) {

    int numOfTests = 20;
    int numberOfShifts = 3000;
    byte[] numbers = new byte[1000000];
    for (int i = 0; i < numbers.length; i++) {
        numbers[i] = (byte) (i % 255);
    }

    System.out.println("Testing method1...");
    BigInteger method1Sum = new BigInteger("00000000", 2);
    for (int i = 1; i <= numOfTests; i++) {
       long total = 0L;
       for (int j = 0; j < numberOfShifts; j++) {
          long startTime = System.nanoTime();
          method1(numbers);
          long endTime   = System.nanoTime();
          total = total + (endTime - startTime);
       }
       method1Sum = method1Sum.add(new BigInteger(Long.toString(total), 10));
       System.out.println(String.format("%-2d: %s", i, getTime(total)));
    }

    System.out.println("Testing method2...");
    BigInteger method2Sum = new BigInteger("00000000", 2);
    for (int i = 1; i <= numOfTests; i++) {
       long total = 0L;
       for (int j = 0; j < numberOfShifts; j++) {
          long startTime = System.nanoTime();
          method2(numbers);
          long endTime   = System.nanoTime();
          total = total + (endTime - startTime);
       }
       method2Sum = method2Sum.add(new BigInteger(Long.toString(total), 10));
       System.out.println(String.format("%-2d: %s", i, getTime(total)));
    }

    System.out.println("method1 average :   " + getTime(method1Sum.longValue() / numOfTests));
    System.out.println("method2 average :   " + getTime(method2Sum.longValue() / numOfTests));
}

private static void method1(byte[] bytes) {
    byte previousByte = bytes[0];
    bytes[0] = (byte) (((bytes[0] & 0xff) >> 1) | ((bytes[bytes.length - 1] & 0xff) << 7));
    for (int i = 1; i < bytes.length; i++) {
       byte tmp = bytes[i];
       bytes[i] = (byte) (((bytes[i] & 0xff) >> 1) | ((previousByte & 0xff) << 7));
       previousByte = tmp;
    }
}

private static void method2(byte[] bytes) {
    byte lastByte = bytes[bytes.length-1];
    for (int i = bytes.length-1; i > 0; i--) {
       bytes[i] = (byte) (((bytes[i] & 0xff) >> 1) | ((bytes[i-1] & 0xff) << 7));
    }
    bytes[0] = (byte) (((bytes[0] & 0xff) >> 1) | ((lastByte & 0xff) << 7));
}

private static String getTime(long nanoSecs) {

  int minutes = (int) (nanoSecs / 60000000000.0);
  int seconds = (int) (nanoSecs / 1000000000.0) - (minutes * 60);
  int millisecs = (int) (((nanoSecs / 1000000000.0) - (seconds + minutes * 60)) * 1000);
  int nanosecs = (int) nanoSecs - (millisecs * 1000000000);

  if (minutes == 0 && seconds == 0 && millisecs == 0) {
     return nanosecs + "ns";
  }

  if (minutes == 0 && seconds == 0) {
     return millisecs + "ms";
  }

  if (minutes == 0 && millisecs == 0) {
     return seconds + "s";
  }

  if (seconds == 0 && millisecs == 0) {
     return minutes + "min";
  }

  if (minutes == 0) {
     return seconds + "s " + millisecs + "ms";
  }

  if (seconds == 0) {
     return minutes + "min " + millisecs + "ms";
  }

  if (millisecs == 0) {
     return minutes + "min " + seconds + "s";
  }

  return minutes + "min " + seconds + "s " + millisecs + "ms";
}
}

更新:

看起来原因是我在第二种方法中的每个循环中访问了 2 个不同的索引,而在第一种方法中我只访问了 1 个索引。所以它与反转循环无关。

感谢@rm5248 和@Ben,如果可以的话,我会选择你们的两个答案,但我选择了较早的一个。

【问题讨论】:

  • @Cory 我在第二种方法中访问 bytes.length 的次数更少,但仍然比第一种方法慢。
  • 哦,哎呀。我读到第二个更快......现在我很困惑。
  • 也许您可以发布您的测试代码?我们假设您的测试是干净的,但我们不能确定。而且,3000次也不算多。如果您运行 1,000 次调用它 100,000 次的测试会怎么样?如果运行时间过长,请减少您正在测试的字节数。
  • @Mysticial 测试数组有100万个元素,实际数组可能是double。
  • 请注意,在讨论性能时,务必说明您使用的是哪个运行时(供应商和版本、CPU 架构以及服务器与桌面)

标签: java performance optimization


【解决方案1】:

我对此进行了快速测试,似乎第二种方法变慢的原因似乎是因为算法发生了一些变化。在第一个中,您将一个值保留在局部变量中,而您不在第二个中。因此,Java 必须两次访问数组才能取出变量。从理论上讲,这应该没有什么不同,但我认为这与数组在 Java 中的实现方式有关(我怀疑如果你在 C 中尝试过,时间会更接近)。

作为参考,这是我的实现(我认为它做同样的事情,但它可能不会):

private static void method2(byte[] bytes) {
    byte prevByte = bytes[bytes.length-1];
    for (int i = bytes.length-1; i > 0; i--) {
        byte tmp = bytes[i];
        bytes[i] = (byte) (((bytes[i] & 0xff) >> 1) | ((prevByte & 0xff) << 7));
        prevByte = tmp;
    }
    bytes[0] = (byte) (((bytes[0] & 0xff) >> 1) | ((bytes[bytes.length-1] & 0xff) << 7));
}

这是我得到的平均时间:

method1 average :   6s 555ms
method2 average :   6s 726ms

【讨论】:

  • “算法改变”是什么意思?从第一种方法更改为第二种方法?如果这就是你的意思,那么如果我注释掉第一种方法,那么根据这个理论,第二种方法应该更快?
  • 除了一个相当大的改变结果的代码 sn-p 之外,这个答案与我在第三段中所说的有什么不同?
  • 是的,这可能是一种不好的表述方式。我的意思是你如何在两种方法之间改变变量。同样,您在第一种方法中有一个局部变量,而在第二种方法中没有。在第一种方法中,您访问数组 1,000,000 次写入和 1,000,000 次读取。在第二种方法中,您访问数组 1,000,000 次写入,但读取 2,000,000 次。在此处保留一个局部变量可以帮助您从数组的读取中减少一百万,这是您的瓶颈发生的地方。 (p.s.我发布的解决方案是错误的)
  • 感谢您的时间@rm5248,但很抱歉我不相信您的推理,即使您从长声明中删除了“阅读”,您也添加了一个以将其存储在局部变量“tmp”。我猜你得到了类似的结果,因为你现在只读取一个索引,而原始方法在每个循环中读取 2 个索引。
【解决方案2】:

这可能是缓存行为,但更可能的解释是 Peter 在他的 cmets 中所说的 - JIT 对第一个代码进行了更好的优化。

具体来说,JIT 很可能会识别出第一个循环的索引永远不会超出数组的边界,因此避免了边界检查。第二个循环更复杂,可能包括对每次访问的边界检查。

除此之外,您的第一个循环仅从数组中读取一个值,另一个从将被注册的临时局部变量中读取。第二个版本从数组中读取两个不同的元素。

要确定答案,您应该查看 JIT 针对这两种情况生成的机器代码的反汇编。

【讨论】:

  • precisejava.com/javaperf/j2se/Loops.htm 有一个有趣的例子,第二个循环(实际上是一个简单的例子)在非 JIT 机器上运行得更快。所以我同意它似乎是优化第一种方法以使其更快的 JIT。 (可能只有上限检查与方法 2 中的上限和下限)。
  • 这是可以解释这种行为的壁橱里的东西。如果我知道如何阅读汇编代码就好了:)
猜你喜欢
  • 1970-01-01
  • 2019-02-07
  • 2017-09-21
  • 2021-10-14
  • 2011-03-20
  • 1970-01-01
  • 2021-03-31
  • 1970-01-01
  • 2019-11-16
相关资源
最近更新 更多