Java 8: Akışların Koleksiyonlara Karşı Performansı


140

Java 8'de yeniyim. Hala API'yi derinlemesine bilmiyorum, ancak yeni Streams API'nin performansını eski iyi Koleksiyonlarla karşılaştırmak için küçük bir gayri resmi karşılaştırma yaptım.

Test, bir listeyi filtrelemek Integerve her çift sayı için kare kökü hesaplamak ve bunun bir sonucu Listolarak saklamaktan oluşur Double.

İşte 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));      
    }.

Ve çift çekirdekli bir makinenin sonuçları:

    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)

Bu özel test için, akışlar koleksiyonların yaklaşık iki katı kadar yavaştır ve paralellik yardımcı olmaz (ya da yanlış şekilde mi kullanıyorum?).

Sorular:

  • Bu test adil mi? Herhangi bir hata yaptım mı?
  • Akışlar koleksiyonlardan daha mı yavaş? Bu konuda iyi bir resmi kıyaslama yapan var mı?
  • Hangi yaklaşım için çabalamalıyım?

Güncelleme sonuçları.

JVM ısınmasından (1k iterasyon) sonra @pveentjer tarafından tavsiye edildiği gibi testi 1k kez çalıştırdım:

    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)

Bu durumda akışlar daha performanslıdır. Filtreleme işlevinin çalışma sırasında yalnızca bir veya iki kez çağrıldığı bir uygulamada ne gözlemleneceğini merak ediyorum.


1
IntStreambunun yerine denedin mi?
Mark Rotteveel

2
Lütfen düzgün ölçebilir misiniz? Eğer tek yaptığınız bir koşuysa, o zaman karşılaştırmalı değerlendirmeleriniz elbette kapalı olacaktır.
skiwi

2
@MisterSmith 1K testleriyle de JVM'nizi nasıl ısıttığınız konusunda şeffaflığımız olabilir mi?
skiwi

1
Ve doğru mikrobenç işaretler yazmakla ilgilenenler için şu soruya bakarsınız: stackoverflow.com/questions/504103/…
Mister Smith

2
@assylias Kullanımı toListfarklı bir iş parçacığı birleştirilmeden önce iş parçacığı olmayan ara listelerde toplanacağı için, iş parçacığı için güvenli olmayan bir listede toplasa bile paralel olarak çalışmalıdır.
Stuart Marks

Yanıtlar:


192
  1. LinkedListYineleyici kullanarak listenin ortasından ağır kaldırma dışında herhangi bir şey için kullanmayı bırakın .

  2. Kıyaslama kodunu elle yazmayı bırakın, JMH kullanın .

Uygun kriterler:

@OutputTimeUnit(TimeUnit.NANOSECONDS)
@BenchmarkMode(Mode.AverageTime)
@OperationsPerInvocation(StreamVsVanilla.N)
public class StreamVsVanilla {
    public static final int N = 10000;

    static List<Integer> sourceList = new ArrayList<>();
    static {
        for (int i = 0; i < N; i++) {
            sourceList.add(i);
        }
    }

    @Benchmark
    public List<Double> vanilla() {
        List<Double> result = new ArrayList<>(sourceList.size() / 2 + 1);
        for (Integer i : sourceList) {
            if (i % 2 == 0){
                result.add(Math.sqrt(i));
            }
        }
        return result;
    }

    @Benchmark
    public List<Double> stream() {
        return sourceList.stream()
                .filter(i -> i % 2 == 0)
                .map(Math::sqrt)
                .collect(Collectors.toCollection(
                    () -> new ArrayList<>(sourceList.size() / 2 + 1)));
    }
}

Sonuç:

Benchmark                   Mode   Samples         Mean   Mean error    Units
StreamVsVanilla.stream      avgt        10       17.588        0.230    ns/op
StreamVsVanilla.vanilla     avgt        10       10.796        0.063    ns/op

Beklediğim gibi, akış uygulaması oldukça yavaştır. JIT tüm lambda malzemelerini satır içine alabilir ancak vanilya sürümü kadar mükemmel özlü kod üretmez.

Genellikle, Java 8 akışları sihir değildir. Zaten iyi uygulanmış şeyleri hızlandıramazlardı (muhtemelen, düz yinelemeler veya her bir ifade için değiştirilen Iterable.forEach()ve çağrılan Java 5'ler Collection.removeIf()). Akışlar daha çok kodlama kolaylığı ve güvenliği ile ilgilidir. Kolaylık - hız dengesi burada çalışıyor.


2
Bunu karşılaştırmak için zaman ayırdığınız için teşekkür ederiz. ArrayList için LinkedList değiştirmenin herhangi bir şeyi değiştireceğini düşünmüyorum, çünkü her iki test de buna eklemeli, zamanlar etkilenmemelidir. Her neyse, sonuçları açıklayabilir misiniz? Burada neyi ölçtüğünüzü söylemek zor (birimler ns / op diyor, ama op olarak kabul edilen nedir?).
Mister Smith

52
Performansla ilgili sonucunuz, geçerli olmakla birlikte, aşırıya kaçmıştır. Akış kodunun yinelemeli koddan daha hızlı olduğu birçok durum vardır, çünkü element başına erişim maliyetleri akışlarla düz yineleyicilerden daha ucuzdur. Ve birçok durumda, akış sürümü elle yazılmış sürümle eşdeğer bir şeye satır çizer. Tabii ki, şeytan ayrıntılarda gizlidir; herhangi bir kod parçası farklı davranabilir.
Brian Goetz

26
@BrianGoetz, akışların daha hızlı olduğu durumlarda lütfen kullanım durumlarını belirtebilir misiniz?
Alexandr

1
@Benchmark@GenerateMicroBenchmark
FMH'nin

3
@BrianGoetz, Akışlar daha hızlı olduğunda kullanım durumlarını belirtebilir misiniz?
kiltek

17

1) Ölçüt kullanarak 1 saniyeden daha kısa bir süre görürsünüz. Bu, yan etkilerin sonuçlarınız üzerinde güçlü bir etkisi olabileceği anlamına gelir. Bu yüzden görevini 10 kat arttırdım

    int max = 10_000_000;

ve kıyas ölçütünüzü yönetti. Benim sonuçlarım:

Collections: Elapsed time:   8592999350 ns  (8.592999 seconds)
Streams: Elapsed time:       2068208058 ns  (2.068208 seconds)
Parallel streams: Elapsed time:  7186967071 ns  (7.186967 seconds)

edit ( int max = 1_000_000) sonuçları olmadan

Collections: Elapsed time:   113373057 ns   (0.113373 seconds)
Streams: Elapsed time:       135570440 ns   (0.135570 seconds)
Parallel streams: Elapsed time:  104091980 ns   (0.104092 seconds)

Sonuçlarınız gibi: akış koleksiyondan daha yavaş. Sonuç: Akım başlatma / iletme değerleri için çok zaman harcandı.

2) Artan görev akışı daha hızlı hale geldi (sorun değil), ancak paralel akış çok yavaş kaldı. Sorun nedir? Not: collect(Collectors.toList())Komutunuz var. Tek bir koleksiyona toplamak, esasen eşzamanlı yürütme durumunda performans darboğazını ve ek yükü getirir. Değiştirerek genel masrafların göreceli maliyetini tahmin etmek mümkündür

collecting to collection -> counting the element count

Akarsu için tarafından yapılabilir collect(Collectors.counting()). Sonuçları aldım:

Collections: Elapsed time:   41856183 ns    (0.041856 seconds)
Streams: Elapsed time:       546590322 ns   (0.546590 seconds)
Parallel streams: Elapsed time:  1540051478 ns  (1.540051 seconds)

Bu büyük bir görev için! ( int max = 10000000) Sonuç: koleksiyona eşya toplamak çok zaman aldı. En yavaş kısım listeye eklenmesidir. BTW, basit ArrayListiçin kullanılır Collectors.toList().


Bu testi mikrobenşaretlemeniz gerekir, yani önce birçok kez ısıtılmalı, daha sonra çok sayıda tmes ve ortalama alınmalıdır.
skiwi

@skiwi, haklısın, özellikle de ölçümlerde büyük bir sapma olduğu için. Sadece temel araştırma yaptım ve sonuçların kesin olduğunu iddia etmiyorum.
Sergey Fedorov

Sunucu modundaki JIT, 10k yürütmelerden sonra devreye girer. Ve sonra kodu derlemek ve değiştirmek biraz zaman alır.
pveentjer

Bu cümlenin hakkında: " Eğer varsa collect(Collectors.toList())sen komuta yani, birçok iş parçacığı tarafından tek Collection ele almak gerektiğinde bir durum söz konusu olabilir. " Neredeyse eminim toListiçin toplar birkaç farklı paralel olarak liste örnekleri. Yalnızca koleksiyondaki son adım olarak öğeler bir listeye aktarılır ve sonra döndürülür. Yani senkronizasyon yükü olmamalıdır. Bu nedenle koleksiyonerlerin hem tedarikçisi, akümülatörü hem de birleştirici işlevi vardır. (Elbette başka nedenlerden dolayı yavaş olabilir.)
Lii

@Lii Ben collectburada uygulama hakkında aynı şekilde düşünüyorum . Ancak sonunda birkaç liste tek bir liste halinde birleştirilmeli ve birleştirme, verilen örnekte en ağır işlem gibi görünüyor.
Sergey Fedorov

4
    public static void main(String[] args) {
    //Calculating square root of even numbers from 1 to N       
    int min = 1;
    int max = 10000000;

    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( doSomeCalculate(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 -> doSomeCalculate(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 ->  doSomeCalculate(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));      
}

static double doSomeCalculate(int input) {
    for(int i=0; i<100000; i++){
        Math.sqrt(i+input);
    }
    return Math.sqrt(input);
}

Kodu biraz değiştirdim, 8 çekirdeği olan mac kitap pro'mda koştum, makul bir sonuç aldım:

Koleksiyonlar: Geçen süre: 1522036826 ns (1.522037 saniye)

Akışlar: Geçen süre: 4315833719 ns (4.315834 saniye)

Paralel akışlar: Geçen süre: 261152901 ns (0.261153 saniye)


Testinizin adil olduğunu düşünüyorum, sadece daha fazla işlemci çekirdeğine sahip bir makineye ihtiyacınız var.
Mellon

3

Yapmaya çalıştığın şey için, normal java api'leri zaten kullanmam. Bir ton boks / kutulama var, bu yüzden büyük bir performans yükü var.

Şahsen ben tasarlanmış bir çok API bok çünkü onlar bir sürü nesne çöp oluşturmak düşünüyorum.

Çift / int ilkel dizilerini kullanmaya çalışın ve bunu tek iş parçacıklı yapmaya ve performansın ne olduğunu görün.

Not: Benchmark yapmak için JMH'ye bakmak isteyebilirsiniz. JVM'yi ısıtmak gibi bazı tipik tuzaklarla ilgilenir.


LinkedLists ArrayLists daha kötü çünkü tüm düğüm nesneleri oluşturmanız gerekir. Mod operatörü de köpek yavaş. Ben 10/15 döngü gibi bir şey inanıyorum + bu talimat boru hattını boşaltır. 2'ye kadar çok hızlı bir bölme yapmak istiyorsanız, 1 biti sağa kaydırmanız yeterlidir. Bunlar temel hileler, ama eminim bazı şeyleri hızlandırmak için mod gelişmiş hileler vardır, ancak bunlar muhtemelen daha soruna özgüdür.
pveentjer

Boksun farkındayım. Bu sadece resmi olmayan bir ölçüttür. Fikir, hem koleksiyonlarda hem de akış testlerinde aynı miktarda boks / kutudan çıkarma.
Bay Smith

İlk olarak, hatayı ölçmediğinden emin olurdum. Gerçek testi yapmadan önce karşılaştırmayı birkaç kez çalıştırmayı deneyin. Sonra en azından JVM ısınma yolumdan var ve kod doğru JITTED. Bu olmadan, muhtemelen yanlış sonuçlar çıkarırsınız.
pveentjer

Tamam, tavsiyelerinizin ardından yeni sonuçlar yayınlayacağım. JMH'ye bir göz attım ama Maven gerektiriyor ve yapılandırması biraz zaman alıyor. Yine de teşekkürler.
Bay Smith

Bence "Yapmaya çalıştığınız şey için" açısından karşılaştırma testleri düşünmekten kaçınmak en iyisidir. yani, genellikle bu tür egzersizler gösterilebilecek kadar basitleştirilmiştir, ancak basitleştirilebilecekleri / olması gerektiği gibi karmaşıktırlar.
ryvantage
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.