web-dev-qa-db-ja.com

バイト配列がすべてゼロかどうかをチェックする最速の方法

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
39
user2555349

最初にすべてのバイトを合計していたので、この回答を書き直しましたが、これはJavaがバイトに署名したので間違っています。したがって、JVMウォームアップを変更する必要があります。今すぐになります。

最善の策は、すべての値を単純にループすることです。

次の3つの主要なオプションが利用できると思います。

  1. またはすべての要素と合計を確認してください。
  2. 分岐のない比較を行います。
  3. ブランチと比較します。

Java(低レベルのパフォーマンス)を使用してバイトを追加することのパフォーマンスがどれほど優れているかわかりません。Javaは(低レベル)ブランチを使用します。分岐比較を行う場合の予測子。

したがって、私は次のことが起こると予想しています:

_byte[] array = new byte[4096];
for (byte b : array) {
    if (b != 0) {
        return false;
    }
}
_
  1. 分岐予測子がまだシードしている最初の数回の反復での比較が比較的遅い。
  2. とにかくすべての値がゼロである必要があるため、分岐予測による非常に高速な分岐比較。

ゼロ以外の値にヒットすると、分岐予測が失敗し、比較の速度が低下しますが、いずれにしてもfalseを返したいため、計算の最後にいます。失敗した分岐予測の1つのコストは、配列の反復を続けるコストほど桁違いに小さいと思います。

さらに、believeは、for (byte b : array)を許可する必要があることを知っています。コードがインライン化されるまで、(リストの反復処理として)余分なメソッド呼び出しを引き起こすPrimitiveArrayIteratorなどはありません。

更新

いくつかの興味深い結果を与える独自のベンチマークを作成しました...残念ながら、既存のベンチマークツールは、正しくインストールするのがかなり難しいため使用できませんでした。

また、オプション1と2を一緒にグループ化することも決めました。実際には、ブランチレスの通常またはすべて(条件を除く)と同じであると思い、最終結果を確認します。そして、ここでの条件は_x > 0_であり、したがって0または0はおそらく無操作です。

コード:

_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ナノ秒であったため、変換を自分で行う必要がある場合は価値がありません。

65
skiwi

これは最速またはほとんどのメモリパフォーマンスソリューションではないかもしれませんが、1つのライナーです。

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

Java 8の場合、単純にこれを使用できます。

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

誰かが一度に4または8バイトをチェックすることを提案しました。あなたは実際にJavaでこれを行うことができます:

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

最適化の可能性が非常に高いため、これがバイト値をチェックするよりも速いかどうかは不明です。

0
VGR

理論的には最速の方法であなたの方法で、実際にはコメンターの1人が示唆するより大きな比較を利用できるかもしれないと思います(1バイトの比較には1命令が必要ですが、64バイトの8バイト比較はビットシステム)。

また、ハードウェア(Cおよびバリアント)に近い言語では、ベクトル化と呼ばれるものを使用して、多数の比較/追加を同時に実行できます。 Javaにはまだネイティブサポートがありませんが、 この回答 に基づいて使用できる場合があります。

また、他のコメントに沿って、4kバッファでは、それを試して最適化する時間はおそらく価値がないと言います(非常に頻繁に呼び出されない限り)

0
Christophe