web-dev-qa-db-fra.com

Quand les flux devraient-ils être préférés aux boucles traditionnelles pour de meilleures performances? Les flux profitent-ils de la prédiction de branche?

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:

  1. 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) 
    
  2. 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:

  1. 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) 
    
  2. 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.

  1. 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? Y a-t-il quelque chose qui me manque?
  2. 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?
  3. Dans aucun des scénarios, je ne pouvais voir les flux profiter de prédiction de branche (J'ai essayé avec des flux triés et non ordonnés, mais sans utilité. Cela a donné plus que le double de l'impact sur les performances par rapport aux flux normaux)?
42
Bandi Kishore

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.

42
Peter Lawrey

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.

27
Timothy Truckle

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.

16
Flown

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.

10
Eugene

Comment mon programme Java s'exécute-t-il rapidement?

Pour faire court, Java peuvent être accélérés par:

  1. Multithreading
  2. JIT

Les flux sont-ils liés à l'accélération du programme Java?

Oui!

  1. Remarque Collection.parallelStream() et Stream.parallel() méthodes de multithreading
  2. On peut écrire for 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 performances

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

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é.

Qu'en est-il de la prédiction de branche?

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.

Alors, quand dois-je réécrire mes boucles en flux pour obtenir les meilleures performances?

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.

4
Sergey Fedorov