检查字节数组是否全为零的最快方法

时间:2014-05-23 08:25:41

标签: java arrays performance primitive

我有一个byte[4096]并且想知道检查所有值是否为零的最快方法是什么?

有没有比做更快的方法:

byte[] b = new byte[4096];
b[4095] = 1;
for(int i=0;i<b.length;i++)
    if(b[i] != 0)
        return false; // Not Empty

5 个答案:

答案 0 :(得分:64)

我已经重写了这个答案,因为我是第一次总结所有字节,但这是不正确的,因为Java已经签名字节,因此我需要或。此外,我已将JVM热备改为现在正确。

你最好的选择就是简单地循环所有值。

我想你有三个主要选择:

  1. 或所有元素并检查总和。
  2. 进行无分支比较。
  3. 与分支机构进行比较。
  4. 我不知道使用Java(低级别性能)添加字节的性能有多好,我知道如果你进行分支比较,Java会使用(低级别)分支预测器。

    因此我预计会发生以下情况:

    byte[] array = new byte[4096];
    for (byte b : array) {
        if (b != 0) {
            return false;
        }
    }
    
    1. 当分支预测器仍处于播种状态时,前几次迭代中的比较相对较慢。
    2. 由于分支预测导致的非常快速的分支比较,因为无论如何每个值都应为零。
    3. 如果它会达到非零值,那么分支预测器会失败,导致比较速度变慢,但是当你想要以任何方式返回false时,你也处于计算结束时。我认为一个失败的分支预测的成本随着继续迭代阵列的成本而小一个数量级。

      我还相信应该允许for (byte b : array),因为它应该直接编译到索引数组迭代中,因为我知道没有PrimitiveArrayIterator这样的东西这将导致一些额外的方法调用(如在列表上进行迭代),直到代码被内联。

      <强>更新

      我写了自己的基准测试,给出了一些有趣的结果......不幸的是我无法使用任何现有的基准测试工具,因为它们很难正确安装。

      我还决定将选项1和2组合在一起,因为我认为它们实际上与您通常的无分支或一切(减去条件)相同,然后检查最终结果。这里的条件是x > 0,因此一个或零是大概是noop。

      代码:

      public class Benchmark {
          private void start() {
              //setup byte arrays
              List<byte[]> arrays = createByteArrays(700_000);
      
              //warmup and benchmark repeated
              arrays.forEach(this::byteArrayCheck12);
              benchmark(arrays, this::byteArrayCheck12, "byteArrayCheck12");
      
              arrays.forEach(this::byteArrayCheck3);
              benchmark(arrays, this::byteArrayCheck3, "byteArrayCheck3");
      
              arrays.forEach(this::byteArrayCheck4);
              benchmark(arrays, this::byteArrayCheck4, "byteArrayCheck4");
      
              arrays.forEach(this::byteArrayCheck5);
              benchmark(arrays, this::byteArrayCheck5, "byteArrayCheck5");
          }
      
          private void benchmark(final List<byte[]> arrays, final Consumer<byte[]> method, final String name) {
              long start = System.nanoTime();
              arrays.forEach(method);
              long end = System.nanoTime();
              double nanosecondsPerIteration = (end - start) * 1d / arrays.size();
              System.out.println("Benchmark: " + name + " / iterations: " + arrays.size() + " / time per iteration: " + nanosecondsPerIteration + "ns");
          }
      
          private List<byte[]> createByteArrays(final int amount) {
              Random random = new Random();
              List<byte[]> resultList = new ArrayList<>();
              for (int i = 0; i < amount; i++) {
                  byte[] byteArray = new byte[4096];
                  byteArray[random.nextInt(4096)] = 1;
                  resultList.add(byteArray);
              }
              return resultList;
          }
      
          private boolean byteArrayCheck12(final byte[] array) {
              int sum = 0;
              for (byte b : array) {
                  sum |= b;
              }
              return (sum == 0);
          }
      
          private boolean byteArrayCheck3(final byte[] array) {
              for (byte b : array) {
                  if (b != 0) {
                      return false;
                  }
              }
              return true;
          }
      
          private boolean byteArrayCheck4(final byte[] array) {
              return (IntStream.range(0, array.length).map(i -> array[i]).reduce(0, (a, b) -> a | b) != 0);
          }
      
          private boolean byteArrayCheck5(final byte[] array) {
              return IntStream.range(0, array.length).map(i -> array[i]).anyMatch(i -> i != 0);
          }
      
          public static void main(String[] args) {
              new Benchmark().start();
          }
      }
      

      令人惊讶的结果:

        

      基准:byteArrayCheck12 / iterations:700000 /每次迭代的时间:50.18817142857143ns
        基准:byteArrayCheck3 / iterations:700000 /每次迭代的时间:767.7371985714286ns
        基准:byteArrayCheck4 / iterations:700000 /每次迭代的时间:21145.03219857143ns
        基准测试:byteArrayCheck5 /迭代:700000 /每次迭代的时间:10376.119144285714ns

      这表明orring比分支预测器快很多,这是相当令人惊讶的,所以我假设正在进行一些低级优化。

      另外,我还包括了流变体,无论如何我都没想到它。

      使用备有时钟的Intel i7-3770,16GB 1600MHz RAM。

      所以我认为最终的答案是:这取决于。这取决于您要连续检查阵列的次数。 &#34; byteArrayCheck3&#34;解决方案总是稳定在700~800ns。

      跟进更新

      实际上事情采取了另一种有趣的方法,结果是JIT几乎所有计算都在优化,因为根本没有使用结果变量。

      因此,我有以下新的benchmark方法:

      private void benchmark(final List<byte[]> arrays, final Predicate<byte[]> method, final String name) {
          long start = System.nanoTime();
          boolean someUnrelatedResult = false;
          for (byte[] array : arrays) {
              someUnrelatedResult |= method.test(array);
          }
          long end = System.nanoTime();
          double nanosecondsPerIteration = (end - start) * 1d / arrays.size();
          System.out.println("Result: " + someUnrelatedResult);
          System.out.println("Benchmark: " + name + " / iterations: " + arrays.size() + " / time per iteration: " + nanosecondsPerIteration + "ns");
      }
      

      这确保了基准测试的结果无法优化,因此主要问题是byteArrayCheck12方法无效,因为它注意到(sum == 0)没有被使用,因此它被优化远离整个方法。

      因此,我们有以下新结果(为清晰起见省略了结果打印):

        

      基准:byteArrayCheck12 / iterations:700000 /每次迭代的时间:1370.6987942857143ns
        基准:byteArrayCheck3 / iterations:700000 /每次迭代的时间:736.1096242857143ns
        基准测试:byteArrayCheck4 / iterations:700000 /每次迭代的时间:20671.230327142857ns
        基准:byteArrayCheck5 /迭代:700000 /每次迭代的时间:9845.388841428572ns

      因此我们认为我们最终可以得出结论,分支预测获胜。然而,它也可能因为早期返回而发生,因为平均而言,有问题的字节将位于字节数组的中间,因此是时候另一种方法不能提前返回:

      private boolean byteArrayCheck3b(final byte[] array) {
          int hits = 0;
          for (byte b : array) {
              if (b != 0) {
                  hits++;
              }
          }
          return (hits == 0);
      }
      

      通过这种方式,我们仍然可以从分支预测中受益,但是我们确保我们不能提前返回。

      这又为我们带来了更有趣的结果!

        

      基准:byteArrayCheck12 / iterations:700000 /每次迭代的时间:1327.2817714285713ns
        基准:byteArrayCheck3 / iterations:700000 /每次迭代的时间:753.31376ns
        基准测试:byteArrayCheck3b /迭代:700000 /每次迭代的时间:1506.6772842857142ns
        基准:byteArrayCheck4 / iterations:700000 /每次迭代的时间:21655.950115714284ns
        基准测试:byteArrayCheck5 /迭代:700000 /每次迭代的时间:10608.70917857143ns

      我认为我们终于可以得出结论,最快的方法是使用早期返回和分支预测,然后是orring,然后是纯粹的分支预测。我怀疑所有这些操作都在本机代码中进行了高度优化。

      更新,使用long和int数组进行一些额外的基准测试。

      在看到有关使用long[]int[]的建议后,我认为值得调查。然而,这些尝试可能不再完全符合原始答案,但仍然可能有趣。

      首先,我更改了benchmark方法以使用泛型:

      private <T> void benchmark(final List<T> arrays, final Predicate<T> method, final String name) {
          long start = System.nanoTime();
          boolean someUnrelatedResult = false;
          for (T array : arrays) {
              someUnrelatedResult |= method.test(array);
          }
          long end = System.nanoTime();
          double nanosecondsPerIteration = (end - start) * 1d / arrays.size();
          System.out.println("Result: " + someUnrelatedResult);
          System.out.println("Benchmark: " + name + " / iterations: " + arrays.size() + " / time per iteration: " + nanosecondsPerIteration + "ns");
      }
      

      然后我在基准测试之前分别从byte[]long[]int[]进行了转换,将最大堆大小设置为10 GB也是必要的

      List<long[]> longArrays = arrays.stream().map(byteArray -> {
          long[] longArray = new long[4096 / 8];
          ByteBuffer.wrap(byteArray).asLongBuffer().get(longArray);
          return longArray;
      }).collect(Collectors.toList());
      longArrays.forEach(this::byteArrayCheck8);
      benchmark(longArrays, this::byteArrayCheck8, "byteArrayCheck8");
      
      List<int[]> intArrays = arrays.stream().map(byteArray -> {
          int[] intArray = new int[4096 / 4];
          ByteBuffer.wrap(byteArray).asIntBuffer().get(intArray);
          return intArray;
      }).collect(Collectors.toList());
      intArrays.forEach(this::byteArrayCheck9);
      benchmark(intArrays, this::byteArrayCheck9, "byteArrayCheck9");
      
      private boolean byteArrayCheck8(final long[] array) {
          for (long l : array) {
              if (l != 0) {
                  return false;
              }
          }
          return true;
      }
      
      private boolean byteArrayCheck9(final int[] array) {
          for (int i : array) {
              if (i != 0) {
                  return false;
              }
          }
          return true;
      }
      

      其结果如下:

        

      基准:byteArrayCheck8 / iterations:700000 /每次迭代的时间:259.8157614285714ns
        基准测试:byteArrayCheck9 /迭代:700000 /每次迭代的时间:266.38013714285717ns

      如果可能以这种格式获取字节,则可能值得探索此路径。但是,当在基准测试方法中进行转换时,每次迭代的时间大约为2000纳秒,因此当您需要自己进行转换时,这是不值得的。

答案 1 :(得分:6)

这可能不是最快或最具记忆力的解决方案,但它只是一个班轮:

byte[] arr = randomByteArray();
assert Arrays.equals(arr, new byte[arr.length]);

答案 2 :(得分:3)

对于Java 8,您只需使用它:

public static boolean isEmpty(final byte[] data){
    return IntStream.range(0, data.length).parallel().allMatch(i -> data[i] == 0);
}

答案 3 :(得分:0)

我认为理论上你的方式是以最快的方式,在实践中你可能能够利用其中一个评论者建议的更大的比较(1字节比较需要1条指令,但是8字节的比较也是如此)一个64位系统)。

此外,在更接近硬件(C和变体)的语言中,您可以使用称为矢量化的东西,您可以同时执行许多比较/添加。看起来Java仍然没有原生支持,但基于this answer,您可能可以使用它。

同样符合其他评论我会说,使用4k缓冲区时,可能不值得花时间尝试优化它(除非它经常被调用)

答案 4 :(得分:0)

有人建议一次检查4或8个字节。你实际上可以用Java做到这一点:

LongBuffer longBuffer = ByteBuffer.wrap(b).asLongBuffer();
while (longBuffer.hasRemaining()) {
    if (longBuffer.get() != 0) {
        return false;
    }
}
return true;

这是否比检查字节值更快是不确定的,因为有很多优化的潜力。