我已经重写了这个答案,因为我首先对所有字节求和,但是这是不正确的,因为 Java 有签名字节,因此我需要 or。此外,我现在已将 JVM 预热更改为正确。
您最好的选择实际上是简单地遍历所有值。
我想你有三个主要的选择:
- 或所有元素并检查总和。
- 进行无分支比较。
- 与分支进行比较。
我不知道使用 Java 添加字节的性能有多好(低级性能),我知道如果您进行分支比较,Java 会使用(低级)分支预测器。
因此,我预计会发生以下情况:
byte[] array = new byte[4096];
for (byte b : array) {
if (b != 0) {
return false;
}
}
- 当分支预测器仍在自行播种时,前几次迭代中的比较相对较慢。
- 由于分支预测,分支比较非常快,因为无论如何每个值都应该为零。
如果它达到一个非零值,那么分支预测器就会失败,导致比较变慢,但是你也处于计算的最后,因为你想以任何一种方式返回 false。我认为一个失败的分支预测的成本比继续迭代数组的成本要小一个数量级。
我进一步相信应该允许for (byte b : array),因为它应该被直接编译到索引数组迭代中,据我所知没有PrimitiveArrayIterator这样的东西会导致一些额外的方法调用(作为迭代列表)直到代码被内联。
更新
我编写了自己的基准测试,它给出了一些有趣的结果...不幸的是,我无法使用任何现有的基准测试工具,因为它们很难正确安装。
我还决定将选项 1 和 2 组合在一起,因为我认为它们实际上与无分支你通常或所有东西(减去条件)相同,然后检查最终结果。这里的条件是x > 0,因此a or of 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 / 迭代次数:700000 / 每次迭代次数:50.18817142857143ns
基准:byteArrayCheck3 / 迭代:700000 / 每次迭代的时间:767.7371985714286ns
基准:byteArrayCheck4 / 迭代次数:700000 / 每次迭代次数:21145.03219857143ns
基准:byteArrayCheck5 / 迭代次数:700000 / 每次迭代时间:10376.119144285714ns
这表明 orring 比分支预测器快很多,这相当令人惊讶,所以我假设正在进行一些低级优化。
作为额外的我已经包含了流变体,无论如何我没想到它会那么快。
在标准时钟 Intel i7-3770、16GB 1600MHz RAM 上运行。
所以我认为最终的答案是:视情况而定。这取决于您要连续检查数组的次数。 “byteArrayCheck3”解决方案始终稳定在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 / 迭代:700000 / 每次迭代的时间:1370.6987942857143ns
基准:byteArrayCheck3 / 迭代:700000 / 每次迭代的时间:736.1096242857143ns
基准:byteArrayCheck4 / 迭代: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 / 迭代:700000 / 每次迭代的时间:1327.2817714285713ns
基准:byteArrayCheck3 / 迭代:700000 / 每次迭代的时间:753.31376ns
基准:byteArrayCheck3b / 迭代:700000 / 每次迭代的时间:1506.6772842857142ns
基准:byteArrayCheck4 / 迭代: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 / 迭代:700000 / 每次迭代的时间:259.8157614285714ns
基准:byteArrayCheck9 / 迭代:700000 / 每次迭代的时间:266.38013714285717ns
如果有可能获取这种格式的字节,这条路径可能值得探索。但是,在基准方法中进行转换时,每次迭代的时间约为 2000 纳秒,因此当您需要自己进行转换时,不值得。