Quand faut-il privilégier les stream par rapport aux boucles traditionnelles pour obtenir les meilleures performances? Les stream profitent-ils de la prédiction de twig?

Je viens de lire à propos de Branch-Prediction et je voulais essayer comment cela fonctionne avec Java 8 Streams.

Cependant, les performances avec Streams se révèlent toujours pires que les boucles traditionnelles.

int totalSize = 32768; int filterValue = 1280; int[] array = new int[totalSize]; Random rnd = new Random(0); int loopCount = 10000; for (int i = 0; i < totalSize; i++) { // array[i] = rnd.nextInt() % 2560; // Unsorted Data array[i] = i; // Sorted Data } long start = System.nanoTime(); long sum = 0; for (int j = 0; j < loopCount; j++) { for (int c = 0; c = filterValue ? array[c] : 0; } } long total = System.nanoTime() - start; System.out.printf("Conditional Operator Time : %d ns, (%f sec) %n", total, total / Math.pow(10, 9)); start = System.nanoTime(); sum = 0; for (int j = 0; j < loopCount; j++) { for (int c = 0; c = filterValue) { sum += array[c]; } } } total = System.nanoTime() - start; System.out.printf("Branch Statement Time : %d ns, (%f sec) %n", total, total / Math.pow(10, 9)); start = System.nanoTime(); sum = 0; for (int j = 0; j  value >= filterValue).sum(); } total = System.nanoTime() - start; System.out.printf("Streams Time : %d ns, (%f sec) %n", total, total / Math.pow(10, 9)); start = System.nanoTime(); sum = 0; for (int j = 0; j  value >= filterValue).sum(); } total = System.nanoTime() - start; System.out.printf("Parallel Streams Time : %d ns, (%f sec) %n", total, total / Math.pow(10, 9)); 

Sortie:

  1. Pour les tableaux sortingés:

     Conditional Operator Time : 294062652 ns, (0.294063 sec) Branch Statement Time : 272992442 ns, (0.272992 sec) Streams Time : 806579913 ns, (0.806580 sec) Parallel Streams Time : 2316150852 ns, (2.316151 sec) 
  2. Pour les tableaux non sortingés:

     Conditional Operator Time : 367304250 ns, (0.367304 sec) Branch Statement Time : 906073542 ns, (0.906074 sec) Streams Time : 1268648265 ns, (1.268648 sec) Parallel Streams Time : 2420482313 ns, (2.420482 sec) 

J’ai essayé le même code en utilisant List :
list.stream() au lieu de Arrays.stream(array)
list.get(c) au lieu du array[c]

Sortie:

  1. Pour la liste sortingée:

     Conditional Operator Time : 860514446 ns, (0.860514 sec) Branch Statement Time : 663458668 ns, (0.663459 sec) Streams Time : 2085657481 ns, (2.085657 sec) Parallel Streams Time : 5026680680 ns, (5.026681 sec) 
  2. Pour la liste non sortingée

     Conditional Operator Time : 704120976 ns, (0.704121 sec) Branch Statement Time : 1327838248 ns, (1.327838 sec) Streams Time : 1857880764 ns, (1.857881 sec) Parallel Streams Time : 2504468688 ns, (2.504469 sec) 

J’ai fait référence à quelques blogs de ce type, qui suggèrent le même problème de performance avec les stream.

  1. Je suis d’accord sur le fait que la programmation avec des stream est plus facile et plus facile pour certains scénarios, mais lorsque nous perdons en performance, pourquoi devons-nous les utiliser? Y a-t-il quelque chose qui me manque?
  2. Quel est le scénario dans lequel les stream sont égaux aux boucles? Est-ce seulement dans le cas où votre fonction définie prend beaucoup de temps, entraînant une performance de boucle négligeable?
  3. Dans aucun des scénarios, je ne pouvais voir les stream tirer parti de la prédiction de twig (j’ai essayé avec des stream sortingés et non ordonnés, mais sans utilité. Cela a plus que doublé l’impact des performances par rapport aux stream normaux)?

    Je suis d’accord sur le fait que la programmation avec des stream est plus facile et plus facile pour certains scénarios, mais lorsque nous perdons en performance, pourquoi devons-nous les utiliser?

    La performance est rarement un problème. Il serait habituel que 10% de vos stream soient réécrits sous forme de boucles pour obtenir les performances dont vous avez besoin.

    Y a-t-il quelque chose qui me manque?

    Utiliser parallelStream () est beaucoup plus facile en utilisant des stream et peut-être plus efficace car il est difficile d’écrire du code concurrent efficace.

    Quel est le scénario dans lequel les stream sont égaux aux boucles? Est-ce seulement dans le cas où votre fonction définie prend beaucoup de temps, entraînant une performance de boucle négligeable?

    Votre benchmark est défectueux dans le sens où le code n’a pas été compilé au démarrage. Je ferais tout le test en boucle comme le fait JMH ou j’utiliserais JMH.

    Dans aucun des scénarios, je ne pouvais voir les stream tirer parti de la prédiction de twig

    La prédiction de twig est une fonctionnalité du processeur et non une fonctionnalité JVM ou stream.

    Java est un langage de haut niveau qui évite au programmeur d’optimiser les performances de bas niveau.

    Ne choisissez jamais une approche spécifique pour des raisons de performances, sauf si vous avez prouvé que cela pose un problème dans votre application réelle.

    Vos mesures montrent un effet négatif sur les stream, mais la différence est inférieure à l’observabilité. Par conséquent, ce n’est pas un problème. En outre, ce test est une situation “synthétique” et le code peut se comporter complètement différemment dans un environnement de production intensif. De plus, le code machine créé à partir de votre code Java (octet) par le JIT peut changer dans les futures versions de Java (maintenance) et rendre vos mesures obsolètes.

    En conclusion: Choisissez la syntaxe ou l’approche qui exprime le mieux votre intention (du programmeur). Gardez la même approche ou la même syntaxe tout au long du programme, sauf si vous avez une bonne raison de changer.

    Tout est dit, mais je veux vous montrer comment votre code devrait ressembler à JMH .

     @Fork(3) @BenchmarkMode(Mode.AverageTime) @Measurement(iterations = 10, timeUnit = TimeUnit.NANOSECONDS) @State(Scope.Benchmark) @Threads(1) @Warmup(iterations = 5, timeUnit = TimeUnit.NANOSECONDS) @OutputTimeUnit(TimeUnit.NANOSECONDS) public class MyBenchmark { private final int totalSize = 32_768; private final int filterValue = 1_280; private final int loopCount = 10_000; // private Random rnd; private int[] array; @Setup public void setup() { array = IntStream.range(0, totalSize).toArray(); // rnd = new Random(0); // array = rnd.ints(totalSize).map(i -> i % 2560).toArray(); } @Benchmark public long conditionalOperatorTime() { long sum = 0; for (int j = 0; j < loopCount; j++) { for (int c = 0; c < totalSize; ++c) { sum += array[c] >= filterValue ? array[c] : 0; } } return sum; } @Benchmark public long branchStatementTime() { long sum = 0; for (int j = 0; j < loopCount; j++) { for (int c = 0; c < totalSize; ++c) { if (array[c] >= filterValue) { sum += array[c]; } } } return sum; } @Benchmark public long streamsTime() { long sum = 0; for (int j = 0; j < loopCount; j++) { sum += IntStream.of(array).filter(value -> value >= filterValue).sum(); } return sum; } @Benchmark public long parallelStreamsTime() { long sum = 0; for (int j = 0; j < loopCount; j++) { sum += IntStream.of(array).parallel().filter(value -> value >= filterValue).sum(); } return sum; } } 

    Les résultats pour un tableau sortingé:

     Benchmark Mode Cnt Score Error Units MyBenchmark.branchStatementTime avgt 30 119833793,881 ± 1345228,723 ns/op MyBenchmark.conditionalOperatorTime avgt 30 118146194,368 ± 1748693,962 ns/op MyBenchmark.parallelStreamsTime avgt 30 499436897,422 ± 7344346,333 ns/op MyBenchmark.streamsTime avgt 30 1126768177,407 ± 198712604,716 ns/op 

    Résultats pour les données non sortingées:

     Benchmark Mode Cnt Score Error Units MyBenchmark.branchStatementTime avgt 30 534932594,083 ± 3622551,550 ns/op MyBenchmark.conditionalOperatorTime avgt 30 530641033,317 ± 8849037,036 ns/op MyBenchmark.parallelStreamsTime avgt 30 489184423,406 ± 5716369,132 ns/op MyBenchmark.streamsTime avgt 30 1232020250,900 ± 185772971,366 ns/op 

    Je peux seulement dire qu’il existe de nombreuses possibilités d’optimisations JVM et peut-être que la prédiction de twig est également impliquée. Il vous appartient maintenant d’interpréter les résultats de référence.

    Je vais append mon 0.02 $ ici.

    Je viens de lire à propos de Branch-Prediction et je voulais essayer comment cela fonctionne avec Java 8 Streams

    Branch Prediction est une fonctionnalité du processeur, qui n’a rien à voir avec la JVM. Il est nécessaire de garder le pipeline du processeur complet et prêt à faire quelque chose. Mesurer ou prédire la prédiction de twig est extrêmement difficile (à moins que vous ne connaissiez réellement les choses EXACTS que le processeur fera). Cela dépendra au moins de la charge actuelle du processeur (qui peut être beaucoup plus que votre programme uniquement).

    Cependant, la performance avec Streams se révèle toujours pire que les boucles traditionnelles

    Cette déclaration et la précédente ne sont pas liées. Oui, les stream seront plus lents pour des exemples simples comme le vôtre, jusqu’à 30% plus lent, ce qui est correct. Vous pouvez mesurer pour un cas particulier à quel point ils sont plus lents ou plus rapides via JMH, comme cela a été suggéré par d’autres, mais cela ne prouve que ce cas, seulement cette charge.

    Dans le même temps, vous travaillerez peut-être avec Spring / Hibernate / Services, etc etc qui font des choses en millisecondes et vos stream en nano-secondes et vous vous souciez des performances? Vous vous interrogez sur la vitesse de votre partie la plus rapide du code? C’est bien sûr une chose théorique.

    Et à propos de votre dernier point que vous avez essayé avec des tableaux sortingés et non sortingés et cela vous donne de mauvais résultats. Ce n’est absolument pas une indication de la prédiction de la twig – vous n’avez aucune idée à quel moment la prédiction a eu lieu et si c’est le cas, à moins de pouvoir regarder à l’intérieur des pipelines de CPU – ce que vous n’avez pas fait.

    Comment mon programme Java peut-il fonctionner rapidement?

    Bref, les programmes Java peuvent être accélérés par:

    1. Multithreading
    2. JIT

    Les stream sont-ils liés à l’accélération du programme Java?

    Oui!

    1. Notez les méthodes Collection.parallelStream() et Stream.parallel() pour le multithreading
    2. On peut écrire for cycle suffisamment long pour que JIT saute. Les Lambdas sont généralement petits et peuvent être compilés par JIT => il est possible de gagner en performance

    Quel est le stream de scénario peut être plus rapide que for boucle?

    Jetons un coup d’oeil à jdk / src / share / vm / runtime / globals.hpp

     develop(intx, HugeMethodLimit, 8000, "Don't comstack methods larger than this if " "+DontComstackHugeMethods") 

    Si vous avez un cycle suffisamment long, il ne sera pas compilé par JIT et fonctionnera lentement. Si vous réécrivez un tel cycle en stream, vous utiliserez probablement les méthodes map , filter , flatMap qui divisent le code en morceaux et chaque élément peut être assez petit pour tenir en-dessous des limites. Bien sûr, écrire des méthodes énormes a d’autres inconvénients que la compilation JIT. Ce scénario peut être considéré si, par exemple, vous avez beaucoup de code généré.

    Qu’en est-il de la prédiction de twig?

    Bien entendu, les stream tirent parti de la prédiction de twig comme tout autre code. Cependant, la prédiction de twig n’est pas la technologie explicitement utilisée pour accélérer les stream AFAIK.

    Alors, quand est-ce que je réécris mes boucles en stream pour obtenir les meilleures performances?

    Jamais.

    L’optimisation prématurée est la racine de tout mal © Donald Knuth

    Essayez plutôt d’optimiser l’algorithme. Les stream constituent l’interface pour une programmation de type fonctionnel, et non un outil pour accélérer les boucles.