Je viens de lire Branch-Prediction et je voulais essayer comment cela fonctionne avec Java 8 Streams.
Cependant, les performances avec Streams s'avèrent toujours moins bonnes 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 < totalSize; ++c) {
sum += array[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 < totalSize; ++c) {
if (array[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 < loopCount; j++) {
sum += Arrays.stream(array).filter(value -> 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 < loopCount; j++) {
sum += Arrays.stream(array).parallel().filter(value -> 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:
Pour le tableau trié:
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)
Pour un tableau non trié:
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 de array[c]
Sortie:
Pour la liste trié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)
Pour la liste non trié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 this & this qui suggèrent le même problème de performance avec les flux.
Je suis d'accord sur le fait que la programmation avec des flux est agréable et plus facile pour certains scénarios, mais lorsque nous perdons des performances, pourquoi devons-nous les utiliser?
La performance est rarement un problème. Il serait habituel que 10% de vos flux doivent être réécrits sous forme de boucles pour obtenir les performances dont vous avez besoin.
Y a-t-il quelque chose qui me manque?
L'utilisation de parallelStream () est beaucoup plus facile à utiliser et peut être plus efficace car il est difficile d'écrire du code concurrent efficace.
Quel est le scénario dans lequel les flux sont égaux aux boucles? Est-ce uniquement dans le cas où votre fonction définie prend beaucoup de temps, résultant en 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 flux profiter de la prédiction de branche
La prédiction de branche est une fonction CPU et non une fonction JVM ou des flux.
Java est un langage de haut niveau qui évite au programmeur d'envisager une optimisation des performances de bas niveau.
Ne choisissez jamais une certaine approche pour des raisons de performances, sauf si vous avez prouvé qu'il s'agit d'un problème dans votre application réelle.
Vos mesures montrent un certain effet négatif pour les cours d'eau, mais la différence est inférieure à l'observabilité. Ce n'est donc pas un problème. De plus, 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 Java (maintenance) et rendre vos mesures obsolètes.
En conclusion: Choisissez la syntaxe ou l'approche qui exprime le plus votre (celle du programmeur) intention . Gardez cette même approche ou syntaxe tout au long du programme, sauf si vous avez une bonne raison de changer.
Tout est dit, mais je veux vous montrer à quoi devrait ressembler votre code en utilisant 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 trié:
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 trié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 branche est également impliquée. A vous maintenant d'interpréter les résultats du benchmark.
J'ajouterai mes 0.02 $ ici.
Je viens de lire sur Branch-Prediction et je voulais essayer comment cela fonctionne avec Java 8 Streams
La prédiction de branche est une fonction CPU, elle n'a rien à voir avec la JVM. Il est nécessaire de garder le pipeline CPU plein et prêt à faire quelque chose. Mesurer ou prédire la prédiction de branche est extrêmement difficile (à moins que vous ne connaissiez réellement les choses EXACTES que le CPU fera). Cela dépendra au moins de la charge que le CPU a actuellement (cela pourrait être beaucoup plus que votre programme uniquement).
Cependant, les performances avec Streams s'avèrent toujours moins bonnes que les boucles traditionnelles
Cette déclaration et la précédente ne sont pas liées. Oui, les flux seront plus lents pour les simples exemples comme le vôtre, jusqu'à 30% plus lent, ce qui est OK. Vous pouvez mesurer pour un cas particulier comment ils sont plus lents ou plus rapides via JMH comme d'autres l'ont suggéré, mais cela ne prouve que ce cas, seulement cette charge.
En même temps, vous peut-être travailler avec Spring/Hibernate/Services, etc etc qui font des choses en millisecondes et vos flux en nano-secondes et vous vous inquiétez 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 triés et non triés et cela vous donne de mauvais résultats. Ce n'est absolument pas une indication de prédiction de branche ou non - vous n'avez aucune idée à quel moment la prédiction s'est produite et si elle l'a fait à moins que vous pouvez regarder à l'intérieur des pipelines CPU réels - ce que vous n'avez pas.
Pour faire court, Java peuvent être accélérés par:
Oui!
Collection.parallelStream()
et Stream.parallel()
méthodes de multithreadingfor
un cycle suffisamment long pour que JIT saute. Les lambdas sont généralement petits et peuvent être compilés par JIT => il y a possibilité d'améliorer les performancesfor
?Jetons un coup d'œil à jdk/src/share/vm/runtime/globals.hpp
develop(intx, HugeMethodLimit, 8000,
"Don't compile methods larger than this if "
"+DontCompileHugeMethods")
Si vous avez un cycle suffisamment long, il ne sera pas compilé par JIT et fonctionnera lentement. Si vous réécrivez un tel cycle pour diffuser, vous utiliserez probablement les méthodes map
, filter
, flatMap
qui divisent le code en morceaux et chaque morceau peut être suffisamment petit pour tenir sous la limite . Bien sûr, l'écriture d'énormes méthodes a d'autres inconvénients en dehors de la compilation JIT. Ce scénario peut être envisagé si, par exemple, vous avez beaucoup de code généré.
Bien sûr, les flux profitent de la prédiction de branche comme tous les autres codes. Cependant, la prédiction de branche n'est pas la technologie explicitement utilisée pour accélérer les flux AFAIK.
Jamais.
L'optimisation prématurée est la racine de tout mal © Donald Knuth
Essayez d'optimiser l'algorithme à la place. Les flux sont l'interface pour une programmation de type fonctionnel, pas un outil pour accélérer les boucles.