L'API Stream de Java est super rapide si elle est bien utilisée

introduction

Java a une fonction appelée ParallelStream qui vous permet d'écrire facilement un traitement parallèle, et si elle est utilisée correctement, les performances peuvent être considérablement améliorées. C'est une fonctionnalité ajoutée dans Java8, donc c'est nouveau, mais récemment j'ai réalisé la bonté des flux parallèles, donc je vais l'écrire.

Environnement d'exploitation

Contenu

Regardons ici l'effet des flux parallèles en prenant comme exemple le code qui vérifie si le nombre de capreca (définition 2) [^ 1] est pour des valeurs numériques de 0 à 600 millions. [^ 1]: Vous n'avez pas besoin de savoir quel est le nombre de capreca. Quoi qu'il en soit, c'est une sorte de traitement qui met une charge sur le processeur. Si vous voulez toujours savoir, veuillez google. De plus, le nombre 600 millions n'a pas de signification particulière. C'est juste le nombre de cas qui prennent du temps à être traités.

Exemple sans flux parallèle

Tout d'abord, regardons un exemple qui n'utilise pas de flux parallèles. (Flux séquentiel) C'est bâclé et long, alors jetez un œil à la méthode principale ci-dessous pour le moment.

import java.util.*;
import java.util.stream.Collectors;
import java.util.stream.LongStream;

public class ParallelStreamSample {
    //Que ce soit le nombre de capreca (définition 2)
    private static boolean isKaprekarNumber2(long n) {
        List<Character> chars = String.valueOf(n)
                                    .chars()
                                    .mapToObj(c -> (char)c)
                                    .collect(Collectors.toList());
        // min
        chars.sort(Comparator.naturalOrder());
        var min = parseLong(join(chars));

        // max
        chars.sort(Comparator.reverseOrder());
        var max = parseLong(join(chars));

        return n == (max - min);
    }

    private static <T> String join(List<T> list) {
        var sb = new StringBuilder();
        for(T item : list) {
            sb.append(item);
        }
        return sb.toString();
    }

    private static long parseLong(String s) {
        if(s.isEmpty()) {
            return 0;
        } else {
            return Long.parseLong(s);
        }
    }

    public static void main(String[] args) {
        System.out.println("--------------------");
        System.out.println("Définition du nombre Capreca 2");
        long start = System.nanoTime();
        LongStream.rangeClosed(0, 600_000_000)
                //Flux séquentiel (généralement cet appel est inutile, mais il est décrit à des fins de comparaison avec un flux parallèle)
                .sequential()
                .filter(n -> n % 9 == 0)
                .filter(ParallelStreamSample::isKaprekarNumber2)
                .forEachOrdered(System.out::println);
        long end = System.nanoTime();

        System.out.printf("temps de traitement(ms): %d\n", (end - start) / 1_000_000);
        System.out.println("--------------------");
    }
}

Dans mon environnement, le résultat est le suivant.

--------------------
Définition du nombre Capreca 2
0
495
6174
549945
631764
63317664
97508421
554999445
temps de traitement(ms): 60284
--------------------

Cela prend un peu plus d'une minute. J'aimerais le rendre un peu plus rapide, mais à quelle vitesse serait-il avec des flux parallèles?

Exemple d'utilisation de flux parallèles

Ensuite, regardons un exemple d'utilisation de flux parallèles.

import java.util.*;
import java.util.stream.Collectors;
import java.util.stream.LongStream;

public class ParallelStreamSample {
    //Que ce soit le nombre de capreca (définition 2)
    private static boolean isKaprekarNumber2(long n) {
        List<Character> chars = String.valueOf(n)
                                    .chars()
                                    .mapToObj(c -> (char)c)
                                    .collect(Collectors.toList());
        // min
        chars.sort(Comparator.naturalOrder());
        var min = parseLong(join(chars));

        // max
        chars.sort(Comparator.reverseOrder());
        var max = parseLong(join(chars));

        return n == (max - min);
    }

    private static <T> String join(List<T> list) {
        var sb = new StringBuilder();
        for(T item : list) {
            sb.append(item);
        }
        return sb.toString();
    }

    private static long parseLong(String s) {
        if(s.isEmpty()) {
            return 0;
        } else {
            return Long.parseLong(s);
        }
    }

    public static void main(String[] args) {
        System.out.println("--------------------");
        System.out.println("Définition du nombre Capreca 2");
        long start = System.nanoTime();
        LongStream.rangeClosed(0, 600_000_000)
                //Flux parallèle
                .parallel()
                .filter(n -> n % 9 == 0)
                .filter(ParallelStreamSample::isKaprekarNumber2)
                .forEachOrdered(System.out::println);
        long end = System.nanoTime();

        System.out.printf("temps de traitement(ms): %d\n", (end - start) / 1_000_000);
        System.out.println("--------------------");
    }
}

Une seule ligne a été modifiée à l'exception du commentaire. (Sequential () est remplacé par parallel ()) Cela vous permet d'utiliser des flux parallèles au lieu de flux séquentiels.

Cela a les résultats suivants dans mon environnement.

--------------------
Définition du nombre Capreca 2
0
495
6174
549945
631764
63317664
97508421
554999445
temps de traitement(ms): 22366
--------------------

Cela fait environ 22 secondes. Le flux séquentiel a pris environ 1 minute, le temps de traitement est donc inférieur à la moitié. C'est merveilleux.

Pourquoi est-ce plus rapide?

En effet, le flux séquentiel n'utilise qu'un seul cœur pour l'exécution, tandis que le flux parallèle utilise plusieurs cœurs pour le traitement partagé. C'est comme fouetter un noyau qui saute pour le faire fonctionner plus rapidement. (Image seulement)

À propos des conditions d'utilisation des flux parallèles

Dans cet exemple, changer une seule ligne l'a accéléré, mais cela ne fonctionne pas toujours de cette façon. Afin de bénéficier de flux parallèles, certaines conditions doivent être remplies.

L'environnement d'exécution est multicœur

C'est une prémisse majeure. Puisqu'il s'agit d'un mécanisme pour le rendre plus rapide en utilisant plusieurs cœurs, il ne sera pas plus rapide dans un environnement à un seul cœur. Au contraire, il est ralenti par la surcharge du traitement parallèle.

Le processeur est le processus de goulot d'étranglement

C'est également une prémisse majeure. C'est une image qui le rend plus rapide en utilisant toutes les ressources du processeur, donc cela n'a pas de sens si quelque chose d'autre que le processeur est le goulot d'étranglement.

Doit être une implémentation thread-safe

Exécuter plusieurs cœurs signifie s'exécuter dans plusieurs threads. Par conséquent, le traitement à effectuer doit être thread-safe. La méthode ʻisKaprekarNumber2 () `ci-dessus est thread-safe car elle ne dépend que des arguments, mais voyons ce qui se passe dans une implémentation non thread-safe qui dépend du champ au lieu de l'argument. (C'est un exemple étrange ...)

import java.util.*;
import java.util.stream.Collectors;
import java.util.stream.LongStream;

public class ThreadUnsafeSample {
    public static void main(String[] args) {
        System.out.println("--------------------");
        System.out.println("Définition du nombre Capreca 2");
        long start = System.nanoTime();

        var myNum = new MyNumber();
        LongStream.rangeClosed(0, 600_000_000)
                .parallel()
                .filter(n -> n % 9 == 0)
                .filter(n -> {
                    //Non thread-safe car il lit et écrit des champs sans contrôle exclusif
                    myNum.setNum(n);
                    return myNum.isKaprekarNumber2();
                })
                .forEachOrdered(System.out::println);
        long end = System.nanoTime();

        System.out.printf("temps de traitement(ms): %d\n", (end - start) / 1_000_000);
        System.out.println("--------------------");
    }

    private static class MyNumber {
        private long num;

        private void setNum(long num) {
            this.num = num;
        }

        //Que ce soit le nombre de capreca (définition 2)
        private boolean isKaprekarNumber2() {
            List<Character> chars = String.valueOf(num)
                                        .chars()
                                        .mapToObj(c -> (char)c)
                                        .collect(Collectors.toList());
            // min
            chars.sort(Comparator.naturalOrder());
            var min = parseLong(join(chars));

            // max
            chars.sort(Comparator.reverseOrder());
            var max = parseLong(join(chars));

            return num == (max - min);
        }

        private <T> String join(List<T> list) {
            var sb = new StringBuilder();
            for(T item : list) {
                sb.append(item);
            }
            return sb.toString();
        }

        private long parseLong(String s) {
            if(s.isEmpty()) {
                return 0;
            } else {
                return Long.parseLong(s);
            }
        }
    }
}

Dans mon environnement, j'ai obtenu les résultats apparemment incorrects suivants:

--------------------
Définition du nombre Capreca 2
temps de traitement(ms): 23902
--------------------

Comme vous pouvez le voir, l'utilisation de flux parallèles dans une implémentation non thread-safe ne fonctionnera pas comme prévu, alors soyez prudent. Bien sûr, si je supprimais parallal () ou le réécrivais en sequential () et l'exécutais dans un flux séquentiel, le résultat était correct. (En plus d'être en retard ...)

--------------------
Définition du nombre Capreca 2
0
495
6174
549945
631764
63317664
97508421
554999445
temps de traitement(ms): 75049
--------------------

Pas de contrôle exclusif

Dans la suite de ce qui précède, si un contrôle exclusif est effectué dans la partie lecture / écriture du champ, le résultat correct sera obtenu.

.filter(n -> {
    synchronized(myNum) {
        myNum.setNum(n);
        return myNum.isKaprekarNumber2();    
    }
})

Voici le résultat de l'exécution.

--------------------
Définition du nombre Capreca 2
0
495
6174
549945
631764
63317664
97508421
554999445
temps de traitement(ms): 90823
--------------------

La sortie est correcte, mais 1,5 fois plus lente que l'implémentation d'origine. Cela est dû au fait que le contrôle exclusif aboutit effectivement à une exécution dans un seul thread (+ il y a une surcharge de l'exécution parallèle). Vous ne savez pas à quoi sert le flux parallèle.

Résumé

Si vous utilisez bien les flux parallèles, vous pouvez vous attendre à une augmentation significative des performances. Cependant, il existe de nombreuses conditions pour obtenir l'effet, alors utilisez-le avec prudence.

De côté

À propos de la partie suivante

// max
chars.sort(Comparator.reverseOrder());

En fait, ce qui suit semble être plus rapide. (À ce stade, chars est trié par ordre croissant)

// max
Collections.reverse(chars);

Cependant, la correction est gênante </ s> et elle n'affecte pas autant le sujet principal, donc je la laisse telle quelle.

Recommended Posts

L'API Stream de Java est super rapide si elle est bien utilisée
Remarques sur l'API Stream et SQL de Java