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

Posted

技术标签:

【中文标题】检查字节数组是不是全为零的最快方法【英文标题】:Fastest way to check if a byte array is all zeros检查字节数组是否全为零的最快方法 【发布时间】:2014-07-12 12:37:44 【问题描述】:

我有一个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

【问题讨论】:

可能不会,但是你觉得这种方式很慢吗?它检查 4k 的内存,谁知道它被编译成什么。除非您要处理大量巨大的数组,否则这可能不是瓶颈。 除了多线程(在这里几乎肯定不会有帮助),没有。 我来自 C 背景 :-) 另一种选择是将所有元素相加并查看总数是否为零,因为每个元素的分支和零测试确实会减慢现代 CPU 的速度 - 但是仅当字节类型只能存储正数而不是负数时才有效... 我不认为 Java 有一个快速的memcmp() 函数来比较内存,您可以将您的数组与预先创建的零 4k 数组进行比较?好的,我现在就闭嘴! @dave 我在想我可以很轻松地在一个int 中添加 4,096 个值,每个值最多 127 个(最大总数为 520,192),最多可以容纳 2,147,483,647 个。跨度> 【参考方案1】:

我已经重写了这个答案,因为我首先对所有字节求和,但是这是不正确的,因为 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 &gt; 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 纳秒,因此当您需要自己进行转换时,不值得。

【讨论】:

+1 用于分析。数学运算实际上仅重载以对 intlong 值进行运算;任何其他类型都提升为ints,因此byte 加法与int 加法一样快。你是正确的,一个 for-each 循环将被编译为一个常规循环。 @user3580294 更新了更多的分析信息,这实际上是相当令人惊讶的! 另外,添加代码的可能优化:也许 OR 字节在一起?因为按原样,具有正字节和负字节可能会导致误报。也许 OR 比添加更快... 你知道,如果我可以多次投票,我会在接下来的一个小时左右这样做。你在这个问题上付出的努力是疯狂的,仅仅投票并不能表明我(可能还有其他读者)多么欣赏你的努力。至于结果,现在结果如何反转非常有趣——似乎 JIT 编译器真的知道如何做它的事情。我想分支预测器毕竟是某种形式的黑魔法。同样有趣的是,即使整个数组在额外的 OR 指令上循环,也会大大减慢计算速度。 @user3580294 你可以悬赏奖励特别优秀的答案。【参考方案2】:

这可能不是最快或内存性能最高的解决方案,但它是单行的:

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

【讨论】:

事实上,这可能是最快的解决方案,因为您可以缓存用于比较的全零数组,因此您无需在每次调用时都创建它。不幸的是,当前的 JVM(JDK 8 r111)似乎没有将Arrays.equals 作为内在函数实现。对于简单的“带有 if check 的循环”版本,我测量的时间约为 0.7 个周期/元素,而 Arrays.equals 版本的时间为 1.1 个周期/迭代。两者都非常快 - 这意味着没有某种类型的矢量化,循环版本平均每个周期约 1.5 个负载,非常接近 2 的理论最大值。 其实看起来只有Arrays.equals(char[], char[])是一个内在的in JDK8,但是在JDK9中,bytechar版本都是are intrinsic。【参考方案3】:

对于 Java 8,您可以简单地使用:

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

【讨论】:

【参考方案4】:

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

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

与其他 cmets 一样,我会说使用 4k 缓冲区可能不值得花时间尝试和优化它(除非经常调用它)

【讨论】:

【参考方案5】:

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

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

return true;

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

【讨论】:

我对此进行了一些基准测试,可以得出结论,它是高性能的,但无法击败byteArrayCheck3b 的代码。而ByteBuffer等是直接映射到JVM中的机器指令的,所以好像不行。再说一次,我也没有在 C 或 C++ 中测试过这种代码。 此外,使用IntBuffer 实际上比LongBuffer 更快。

以上是关于检查字节数组是不是全为零的最快方法的主要内容,如果未能解决你的问题,请参考以下文章

诚之和:Numpy怎么检查数组全为零的几种方法

将现有数组归零的最快方法是啥?

c语言:生产元素全为零的数组zero[100],将一个整数1随机放入,采用顺序查找法查找并记录步数,

比较两个字节数组的最快方法是啥?

在opencv中使用imshow显示字节数组的最快方法是啥?

在 F# 中比较两个字节数组的最快方法