Java 8 Streams Geschwindigkeit

Hallo Leute,

ich arbeite mich gerade etwas in Java 8 ein und probiere eigentlich nur etwas herum. Dazu habe ich eine CSV-Datei mit 10.000 Datensätzen zu Personen, welche
ich zuvor in eine Liste einlese.

Jetzt wollte ich gerade die Geschwindigkeit vergleichen zwischen einem parallelisierten Stream und der üblichen Liste, dazu mal mein Code, vielleicht könnt ihr mir sagen, womit das zusammenhängt:

Stream:

long startTimeOfStreamOperation = System.currentTimeMillis();
System.out.print(stream.filter(p -> !p.isMale()).count() + "	");
long endTimeOfStreamOperation = System.currentTimeMillis();
System.out.println((endTimeOfStreamOperation - startTimeOfStreamOperation) + " ms");

Liste:

for(Person p : persons) {
    if(!p.isMale()) counter++;
}
System.out.print(counter + "	");
long endTimeOfListOperation = System.currentTimeMillis();
System.out.println((endTimeOfListOperation - startTimeOfListOperation) + " ms");

Die Ausgabe sieht wie folgt aus:


4401	177 ms
4401	4 ms

Gibt es da irgendwie einen schnelleren Weg, ich verwende anscheinend eine falsche Methode, weil bei filter ein neuer Stream erzeugt wird für alle gültigen Datensätze.

Das sind die Wunder von Java8 um mit dem “parallel()” halbwegs klarzukommen muss man erstens Wissen wie das Fork-Join-Framework funktioniert und zudem einen guten Überblick haben welche Operationen sich überhaupt vernünftig parallelisieren lassen. Ich war zu Beginn von der “parallel”-Geschichte angetan, jetzt bin ich vorsichtig bei diesem Thema. Viele Probleme werden unter http://coopsoft.com/ar/Calamity2Article.html besprochen.

Dein spezifisches Problem hier, ist das die zugrundeliegende Mechanik eine Warm-up-Phase benötigt, würdest du den Teil mit dem parallel-Stream doppeln und auf z.B. einem anderen Stream mit anderem Filter laufen lassen wäre der zweite Run etwas schneller als die for-Schleife.

Unten ein schnell zusammengestückeltes Beispiel, dass das demonstriert.

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
import java.util.stream.Stream;

public class Main {
    public static List<Character> generateAlphabet(){
        List<Character> alphabet = new ArrayList<>();
        for(char c : "abcdefghijklmnopqrstuvwxyz".toCharArray()){
            alphabet.add(c);
        }
        return alphabet;
    }

    public static String toString(List<Character> list){
        StringBuilder sb = new StringBuilder();
        list.stream().forEach(c -> sb.append(c.charValue()));
        return sb.toString();
    }

    public static Stream<String> getScrambledAbcStream(int elements){
        List<Character> alphabet = generateAlphabet();
        return IntStream.range(0,elements).mapToObj(i -> {
            Collections.shuffle(alphabet);
            return toString(alphabet);
        });
    }

    public static long time(Runnable runnable){
        long start = System.nanoTime();
        runnable.run();
        return (System.nanoTime()-start) / 1_000_000;
    }

    public static void main(String...args){

        int elements = 100000;

        List<String> list = getScrambledAbcStream(elements).collect(Collectors.toList());
        System.out.println(time(()->{int counter = 0;
                                     for(String p : list) {
                                        if(!p.startsWith("g")) counter++;
                                     }
                                     System.out.print(counter + "	");
                                    }) + " ms");

        System.out.println(time(()-> System.out.print(list
                .parallelStream()
                .filter(s -> !s.startsWith("g"))
                .count() + "	")) + " ms");

        Stream<String> mystream = getScrambledAbcStream(elements)
                                    .collect(Collectors.toList())
                                    .parallelStream();

        System.out.println(time(()->{System.out.print(mystream.filter(s -> s.endsWith("h")).count()+ "	");}) + " ms");
    }
}

[QUOTE=BinaryLogic]
…ich verwende anscheinend eine falsche Methode, weil bei filter ein neuer Stream erzeugt wird für alle gültigen Datensätze.[/QUOTE]

Das ist normal, und auch Sinn der Sache.

Der wichtigste Punkt wurde von ThreadPool schon angedeutet, aber nochmal ganz explizit: So ein Test, bei dem man einen Methodenaufruf in zwei System.currentTimeMillis-Aufrufe einwickelt, macht praktisch nie irgendeinen Sinn. Und das ist unabhängig von Java8 und Streams.

Ganz allgemein geht sowas ja in Richtung eines Microbenchmarks, und aus einem solchen Microbenchmark auch nur halbwegs vernünftige Informationen zu ziehen, ist schwierig. Man kann da zwar https://code.google.com/p/caliper/ oder http://openjdk.java.net/projects/code-tools/jmh/ drauf loslassen, aber das ist dann doch auch oft ein Overkill.

Ein “Muster”, mit den man so einem Test oft zumindest einen Hauch von Aussagekraft verleihen kann, ist grob:

public static void runTest()
{
    for (int dataSize = 10000; dataSize<=10000*100; dataSize*=10)
    {
        InputData input = createInputData(dataSize);

        long durationA = 0;
        long durationB = 0;
        for (int run=0; run<5; run++)
        {
            long beforeA = System.nanoTime();
            OutputData outputA = runMethodA(inputData);
            long afterA = System.nanoTime();
            durationA += (afterA-beforeA);
            System.out.println("outputA: "+outputA); 

            long beforeB = System.nanoTime();
            OutputData outputB = runMethodB(inputData);
            long afterB = System.nanoTime();
            durationB += (afterB-beforeB);
            System.out.println("outputB: "+outputB); 
        }
        System.out.println("For "+dataSize+" A took "+(durationA/1e6/runs)+"ms"); 
        System.out.println("For "+dataSize+" B took "+(durationA/1e6/runs)+"ms"); 
    }
}

InputData/OutputData stehen dabei für “irgendwelche” Ein/Ausgaben (in deinem Beispiel: Eine Liste von Personen, und der “count”), und die Ergebnisse sollten ausgegeben werden, damit der Aufruf nicht komplett wegoptimiert werden kann.

Das ist wirklich nur ein GANZ grobes Muster, und die Ergebnisse immer noch mit Vorsicht zu genießen, aber sie können zumindest eine Tendenz aufzeigen.

Ja, vielen Dank euch beiden. Ich hatte da glaube sogar letztens erst einen Artikel im (glaube) JavaMagazin gelesen. Da ging es
um Performance von zufällig generierten Ziffern. Hatte es schon wieder verdrängt.

Danke auch für die Links, vor allem bei Coopsoft werde ich nochmal etwas mehr nachlesen.