Jestem nowy w Javie 8. Nadal nie znam dokładnie API, ale zrobiłem mały nieformalny test porównawczy, aby porównać wydajność nowego API Streams ze starymi, dobrymi kolekcjami.
Badanie polega na filtrowanie listy Integer
i dla każdego numeru nawet obliczyć pierwiastek kwadratowy i przechowywanie go w rezultacie List
o Double
.
Oto kod:
public static void main(String[] args) {
//Calculating square root of even numbers from 1 to N
int min = 1;
int max = 1000000;
List<Integer> sourceList = new ArrayList<>();
for (int i = min; i < max; i++) {
sourceList.add(i);
}
List<Double> result = new LinkedList<>();
//Collections approach
long t0 = System.nanoTime();
long elapsed = 0;
for (Integer i : sourceList) {
if(i % 2 == 0){
result.add(Math.sqrt(i));
}
}
elapsed = System.nanoTime() - t0;
System.out.printf("Collections: Elapsed time:\t %d ns \t(%f seconds)%n", elapsed, elapsed / Math.pow(10, 9));
//Stream approach
Stream<Integer> stream = sourceList.stream();
t0 = System.nanoTime();
result = stream.filter(i -> i%2 == 0).map(i -> Math.sqrt(i)).collect(Collectors.toList());
elapsed = System.nanoTime() - t0;
System.out.printf("Streams: Elapsed time:\t\t %d ns \t(%f seconds)%n", elapsed, elapsed / Math.pow(10, 9));
//Parallel stream approach
stream = sourceList.stream().parallel();
t0 = System.nanoTime();
result = stream.filter(i -> i%2 == 0).map(i -> Math.sqrt(i)).collect(Collectors.toList());
elapsed = System.nanoTime() - t0;
System.out.printf("Parallel streams: Elapsed time:\t %d ns \t(%f seconds)%n", elapsed, elapsed / Math.pow(10, 9));
}.
A oto wyniki dla maszyny dwurdzeniowej:
Collections: Elapsed time: 94338247 ns (0,094338 seconds)
Streams: Elapsed time: 201112924 ns (0,201113 seconds)
Parallel streams: Elapsed time: 357243629 ns (0,357244 seconds)
W tym konkretnym teście strumienie są około dwa razy wolniejsze niż kolekcje, a równoległość nie pomaga (lub używam go w niewłaściwy sposób?).
Pytania:
- Czy ten test jest sprawiedliwy? Czy popełniłem jakiś błąd?
- Czy strumienie są wolniejsze niż zbiory? Czy ktoś zrobił w tej sprawie dobry formalny punkt odniesienia?
- Do jakiego podejścia powinienem dążyć?
Zaktualizowane wyniki.
Przeprowadziłem test 1k razy po rozgrzewce JVM (1k iteracji), zgodnie z radą @pveentjer:
Collections: Average time: 206884437,000000 ns (0,206884 seconds)
Streams: Average time: 98366725,000000 ns (0,098367 seconds)
Parallel streams: Average time: 167703705,000000 ns (0,167704 seconds)
W tym przypadku strumienie są bardziej wydajne. Zastanawiam się, co można by zaobserwować w aplikacji, w której funkcja filtrująca jest wywoływana tylko raz lub dwa razy w czasie działania.
źródło
IntStream
zamiast tego?toList
powinno działać równolegle, nawet jeśli jest zbierane do listy, która nie jest bezpieczna dla wątków, ponieważ różne wątki będą gromadzić się na listach pośrednich ograniczonych wątkami przed scaleniem.Odpowiedzi:
Przestań używać
LinkedList
do niczego poza ciężkim usuwaniem ze środka listy za pomocą iteratora.Przestań pisać ręcznie kod do testów porównawczych, użyj JMH .
Właściwe wzorce:
Wynik:
Tak jak się spodziewałem, implementacja strumienia jest dość wolniejsza. JIT jest w stanie wbudować wszystkie elementy lambda, ale nie tworzy tak idealnie zwięzłego kodu, jak wersja waniliowa.
Generalnie strumienie Java 8 nie są magiczne. Nie mogli przyspieszyć już dobrze zaimplementowanych rzeczy (prawdopodobnie z prostymi iteracjami lub instrukcjami for-each w Javie 5 zamienionymi na
Iterable.forEach()
iCollection.removeIf()
wywołania). W strumieniach chodzi bardziej o wygodę i bezpieczeństwo kodowania. Wygoda - tu działa kompromis szybkości.źródło
@Benchmark
zamiast@GenerateMicroBenchmark
1) Korzystając z testu porównawczego, widzisz czas krótszy niż 1 sekunda. Oznacza to, że efekty uboczne mogą mieć silny wpływ na Twoje wyniki. Więc 10 razy zwiększyłem twoje zadanie
i przeprowadź test porównawczy. Moje wyniki:
bez edit (
int max = 1_000_000
) wyniki byłyTo jak Twoje wyniki: strumień jest wolniejszy niż zbieranie. Wniosek: zainicjowanie strumienia / przesłanie wartości zajęło dużo czasu.
2) Po zwiększeniu strumień zadań stał się szybszy (to OK), ale strumień równoległy pozostał zbyt wolny. Co jest nie tak? Uwaga: masz
collect(Collectors.toList())
w sobie dowództwo. Zbieranie do pojedynczej kolekcji zasadniczo wprowadza wąskie gardło wydajności i obciążenie w przypadku równoczesnego wykonywania. Możliwe jest oszacowanie względnego kosztu narzutów poprzez wymianęW przypadku strumieni można to zrobić za pomocą
collect(Collectors.counting())
. Otrzymałem wyniki:To duże zadanie! (
int max = 10000000
) Wniosek: zbieranie przedmiotów do kolekcji zajęło większość czasu. Najwolniejsze jest dodawanie do listy. BTW, simpleArrayList
jest używany doCollectors.toList()
.źródło
collect(Collectors.toList())
w sobie polecenie, tj. Może zaistnieć sytuacja, gdy będziesz musiał adresować jedną kolekcję wieloma wątkami. ” Jestem prawie pewien, żetoList
zbiera się równolegle do kilku różnych instancji list. Dopiero jako ostatni krok w kolekcji elementy są przenoszone na jedną listę, a następnie zwracane. Więc nie powinno być narzutu synchronizacji. Dlatego zbieracze mają zarówno funkcję dostawcy, jak i sumatora. (Oczywiście może to być powolne z innych powodów).collect
implementacji. Ale ostatecznie kilka list powinno zostać połączonych w jedną i wygląda na to, że scalanie jest najtrudniejszą operacją w podanym przykładzie.Trochę zmieniam kod, uruchomiłem na moim Mac Book Pro z 8 rdzeniami, uzyskałem rozsądny wynik:
Kolekcje: czas, który upłynął: 1522036826 ns (1,522037 sekund)
Strumienie: czas, który upłynął: 4315833719 ns (4,315834 sekund)
Strumienie równoległe: czas, który upłynął: 261152901 ns (0,261153 sekundy)
źródło
Do tego, co próbujesz zrobić, i tak nie użyłbym zwykłych interfejsów API Java. Jest mnóstwo boksu / unboxingu, więc jest ogromny narzut wydajności.
Osobiście uważam, że wiele zaprojektowanych API to bzdury, ponieważ tworzą dużo zaśmiecania obiektów.
Spróbuj użyć prymitywnych tablic double / int i spróbuj zrobić to w jednym wątku i zobacz, jaka jest wydajność.
PS: Możesz rzucić okiem na JMH, aby zająć się wykonaniem testu porównawczego. Rozwiązuje niektóre typowe pułapki, takie jak rozgrzewanie JVM.
źródło