web-dev-qa-db-fra.com

Moyen le plus rapide de vérifier si un tableau d'octets est entièrement composé de zéros

J'ai un byte[4096] et se demandait quel est le moyen le plus rapide pour vérifier si toutes les valeurs sont nulles?

Y a-t-il un moyen plus rapide que de le faire:

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

J'ai réécrit cette réponse car je sommais tout d'abord tous les octets, ce qui est cependant incorrect car Java a signé des octets, donc je dois ou. J'ai également changé le préchauffage JVM en être correct maintenant.

Votre meilleur pari est vraiment de simplement parcourir toutes les valeurs.

Je suppose que vous avez trois options principales disponibles:

  1. Ou tous les éléments et vérifiez la somme.
  2. Faites des comparaisons sans branche.
  3. Faites des comparaisons avec une branche.

Je ne sais pas à quel point les performances sont bonnes en ajoutant des octets en utilisant Java (performances de bas niveau), je sais que Java utilise la branche (bas niveau)) prédicteurs si vous donnez des comparaisons ramifiées.

Par conséquent, je m'attends à ce que les événements suivants se produisent:

byte[] array = new byte[4096];
for (byte b : array) {
    if (b != 0) {
        return false;
    }
}
  1. Comparaison relativement lente dans les premières itérations lorsque le prédicteur de branche est toujours en train de se semer.
  2. Comparaisons de branche très rapides en raison de la prédiction de branche car chaque valeur doit être nulle de toute façon.

S'il atteignait une valeur non nulle, le prédicteur de branche échouerait, entraînant un ralentissement de la comparaison, mais vous êtes également à la fin de votre calcul car vous souhaitez retourner faux dans les deux cas. Je pense que le coût d'une prédiction de branche défaillante est d'un ordre de grandeur plus petit que le coût de la poursuite de l'itération sur le tableau.

De plus je crois que for (byte b : array) devrait être autorisé car il devrait être compilé directement dans l'itération du tableau indexé pour autant que je sache rien de tel qu'un PrimitiveArrayIterator qui provoquerait des appels de méthode supplémentaires (comme l'itération sur une liste) jusqu'à ce que le code soit en ligne.

Mise à jour

J'ai écrit mes propres benchmarks qui donnent des résultats intéressants ... Malheureusement, je n'ai pu utiliser aucun des outils de benchmark existants car ils sont assez difficiles à installer correctement.

J'ai également décidé de regrouper les options 1 et 2, car je pense qu'elles sont en fait les mêmes que pour vous sans branche habituellement ou tout (moins la condition), puis vérifiez le résultat final. Et la condition ici est x > 0 Et donc un ou de zéro est probablement un noop.

Le code:

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();
    }
}

Les résultats surprenants:

Indice de référence: byteArrayCheck12/itérations: 700000/heure par itération: 50.18817142857143ns
Indice de référence: byteArrayCheck3/itérations: 700000/heure par itération: 767.7371985714286ns
Indice de référence: byteArrayCheck4/itérations: 700000/heure par itération: 21145.03219857143ns
Indice de référence: byteArrayCheck5/itérations: 700000/heure par itération: 10376.119144285714ns

Cela montre que orring est beaucoup plus rapide que le prédicteur de branche, ce qui est plutôt surprenant, donc je suppose que des optimisations de bas niveau sont en cours.

En plus, j'ai inclus les variantes de flux, que je ne m'attendais pas à être aussi rapide de toute façon.

Fonctionné sur un Intel i7-3770 cadencé, 16 Go de RAM 1600 MHz.

Je pense donc que la réponse finale est: cela dépend. Cela dépend du nombre de fois où vous allez vérifier la baie consécutivement. La solution "byteArrayCheck3" est toujours stable à 700 ~ 800ns.

Mise à jour de suivi

Les choses prennent en fait une autre approche intéressante, il s'avère que le JIT optimisait presque tous les calculs, car les variables résultantes n'étaient pas utilisées du tout.

J'ai donc la nouvelle méthode benchmark suivante:

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");
}

Cela garantit que le résultat des benchmarks ne peut pas être optimisé, le problème majeur était donc que la méthode byteArrayCheck12 Était nulle, car elle a remarqué que le (sum == 0) N'était pas utilisé, donc elle a été optimisée toute la méthode.

Ainsi, nous avons le nouveau résultat suivant (omis le résultat imprime pour plus de clarté):

Indice de référence: byteArrayCheck12/itérations: 700000/heure par itération: 1370.6987942857143ns
Indice de référence: byteArrayCheck3/itérations: 700000/heure par itération: 736.1096242857143ns
Indice de référence: byteArrayCheck4/itérations: 700000/heure par itération: 20671.230327142857ns
Indice de référence: byteArrayCheck5/itérations: 700000/heure par itération: 9845.388841428572ns

Par conséquent, nous pensons que nous pouvons enfin conclure que la prédiction de branche gagne. Cela pourrait cependant aussi se produire en raison des premiers retours, car en moyenne l'octet incriminé sera au milieu du tableau d'octets, il est donc temps pour une autre méthode qui ne retourne pas tôt:

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

De cette façon, nous bénéficions toujours de la prédiction de branche, mais nous nous assurons que nous ne pouvons pas revenir tôt.

Ce qui nous donne à nouveau des résultats plus intéressants!

Indice de référence: byteArrayCheck12/itérations: 700000/heure par itération: 1327.2817714285713ns
Indice de référence: byteArrayCheck3/itérations: 700000/heure par itération: 753.31376ns
Indice de référence: byteArrayCheck3b/itérations: 700000/heure par itération: 1506.6772842857142ns
Indice de référence: byteArrayCheck4/itérations: 700000/heure par itération: 21655.950115714284ns
Indice de référence: byteArrayCheck5/itérations: 700000/heure par itération: 10608.70917857143ns

Je pense que nous pouvons cependant conclure que le moyen le plus rapide consiste à utiliser à la fois le retour anticipé et la prédiction de branche, suivi de orring, suivi de prédiction purement de branche. Je soupçonne que toutes ces opérations sont hautement optimisées en code natif.

pdate, quelques benchmarking supplémentaires utilisant des tableaux longs et int.

Après avoir vu des suggestions sur l'utilisation de long[] Et int[], J'ai décidé que cela valait la peine d'être étudié. Cependant, ces tentatives peuvent ne plus être entièrement conformes aux réponses originales, mais peuvent néanmoins être intéressantes.

Tout d'abord, j'ai changé la méthode benchmark pour utiliser des génériques:

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");
}

Ensuite, j'ai effectué des conversions de byte[] À long[] Et int[] Respectivement avant les repères, il était également nécessaire de définir la taille maximale du tas à 10 Go .

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;
}

Ce qui a donné les résultats suivants:

Indice de référence: byteArrayCheck8/itérations: 700000/heure par itération: 259.8157614285714ns
Indice de référence: byteArrayCheck9/itérations: 700000/heure par itération: 266.38013714285717ns

Ce chemin peut être intéressant à explorer s'il est possible d'obtenir les octets dans un tel format. Cependant, lorsque vous effectuez les transformations à l'intérieur de la méthode de référence, le temps était d'environ 2000 nanosecondes par itération, donc cela ne vaut pas la peine lorsque vous devez effectuer les conversions vous-même.

65
skiwi

Ce n'est peut-être pas la solution la plus rapide ou la plus performante en mémoire, mais c'est une seule ligne:

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

Pour Java 8, vous pouvez simplement utiliser ceci:

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

Quelqu'un a suggéré de vérifier 4 ou 8 octets à la fois. Vous pouvez réellement le faire en Java:

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

Que cela soit plus rapide que la vérification des valeurs d'octets est incertain, car il y a tellement de potentiel d'optimisation.

0
VGR

Je pense que théoriquement, votre chemin de la manière la plus rapide, dans la pratique, vous pourriez être en mesure d'utiliser des comparaisons plus grandes comme suggéré par l'un des commentateurs (une comparaison de 1 octet prend 1 instruction, mais il en va de même pour une comparaison de 8 octets sur un 64- système de bits).

Dans les langues plus proches du matériel (C et variantes), vous pouvez également utiliser quelque chose appelé vectorisation, où vous pouvez effectuer plusieurs comparaisons/ajouts simultanément. Il semble que Java n'a toujours pas de support natif, mais basé sur cette réponse vous pourrez peut-être vous en servir.

Toujours en accord avec les autres commentaires, je dirais qu'avec un tampon 4k, cela ne vaut probablement pas le temps d'essayer de l'optimiser (à moins qu'il ne soit appelé très souvent)

0
Christophe