जावा 8: स्ट्रीम बनाम कलेक्शन का प्रदर्शन


140

मैं जावा 8 में नया हूं। मुझे अभी भी एपीआई की गहराई से जानकारी नहीं है, लेकिन मैंने नई स्ट्रीम एपीआई बनाम अच्छे पुराने संग्रह के प्रदर्शन की तुलना करने के लिए एक छोटा अनौपचारिक बेंचमार्क बनाया है।

परीक्षण में एक सूची को छानने के लिए होता है Integer, और प्रत्येक सम संख्या के लिए, वर्गमूल की गणना करता है और इसके परिणामस्वरूप इसे संग्रहीत करता Listहै Double

यहाँ कोड है:

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

और यहाँ एक दोहरी कोर मशीन के लिए परिणाम हैं:

    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)

इस विशेष परीक्षण के लिए, धाराएं संग्रह की तुलना में दो गुना धीमी हैं, और समानतावाद मदद नहीं करता है (या तो मैं इसे गलत तरीके से उपयोग कर रहा हूं?)।

प्रशन:

  • क्या यह परीक्षण उचित है? क्या मैंने कोई गलती की है?
  • क्या संग्रह की तुलना में धाराएँ धीमी हैं? क्या किसी ने इस पर एक अच्छा औपचारिक बेंचमार्क बनाया है?
  • मुझे किस दृष्टिकोण के लिए प्रयास करना चाहिए?

अपडेट किए गए परिणाम।

जेवीएम वार्मअप (1k पुनरावृत्तियों) के बाद मैंने @pveentjer द्वारा सलाह के अनुसार परीक्षण 1k बार चलाया:

    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)

इस मामले में धाराएँ अधिक प्रदर्शनकारी हैं। मुझे आश्चर्य है कि एक ऐप में क्या देखा जाएगा जहां फ़िल्टरिंग फ़ंक्शन केवल रनटाइम के दौरान एक या दो बार कहा जाता है।


1
क्या आपने IntStreamइसके बजाय कोशिश की है ?
मार्क रोटेवेल 16

2
क्या आप कृपया ठीक से माप सकते हैं? यदि आप सब कर रहे हैं तो एक रन है, तो आपके बेंचमार्क निश्चित रूप से बंद हो जाएंगे।
स्किवि

2
@ मिस्टरस्मिथ क्या हमारे पास आपके जेवीएम को गर्म करने के बारे में कुछ पारदर्शिता हो सकती है, वह भी 1K परीक्षणों के साथ?
skiwi

1
और सही माइक्रोबैनचक्र्स लिखने में रुचि रखने वालों के लिए, इस सवाल का जवाब देते हैं: stackoverflow.com/questions/504103/…
मिस्टर स्मिथ

2
@assylias toListसमानांतर में चलना चाहिए भले ही यह एक गैर-थ्रेड-सुरक्षित सूची में एकत्रित हो, क्योंकि विभिन्न धागे विलय होने से पहले थ्रेड-सीमित इंटरमीडिएट सूचियों में एकत्रित हो जाएंगे।
स्टुअर्ट मार्क्स

जवाबों:


192
  1. LinkedListकुछ भी उपयोग करने के लिए बंद करो, लेकिन सूची का उपयोग कर सूची के बीच से भारी हटाने।

  2. हाथ से बेंचमार्किंग कोड लिखना बंद करें, JMH का उपयोग करें

उचित बेंचमार्क:

@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)));
    }
}

परिणाम:

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

जैसा कि मैंने उम्मीद की थी कि स्ट्रीम कार्यान्वयन काफी धीमा है। JIT सभी लैम्बडा सामान को इनलाइन करने में सक्षम है, लेकिन वेनिला संस्करण के रूप में पूरी तरह से संक्षिप्त कोड का उत्पादन नहीं करता है।

आम तौर पर, जावा 8 स्ट्रीम जादू नहीं हैं। वे पहले से ही अच्छी तरह से कार्यान्वित चीजों (साथ, शायद, सादे पुनरावृत्तियों या जावा 5 के प्रत्येक-बयानों को कॉल Iterable.forEach()और Collection.removeIf()कॉल के साथ बदल दिया ) को गति नहीं दे सकते । स्ट्रीम कोडिंग सुविधा और सुरक्षा के बारे में अधिक हैं। सुविधा - स्पीड ट्रेडऑफ यहां काम कर रही है।


2
इस बेंच के लिए समय निकालने के लिए धन्यवाद। मुझे नहीं लगता है कि ArrayList के लिए LinkedList बदलने से कुछ भी बदल जाएगा, क्योंकि दोनों परीक्षणों को इसे जोड़ना चाहिए, समय प्रभावित नहीं होना चाहिए। वैसे भी, क्या आप कृपया परिणाम बता सकते हैं? यह बताना मुश्किल है कि आप यहां क्या माप रहे हैं (इकाइयां ns ​​/ op कहती हैं, लेकिन क्या एक op माना जाता है?)।
मिस्टर स्मिथ

52
प्रदर्शन के बारे में आपका निष्कर्ष, जबकि वैध है, ओवरब्लाक है। ऐसे बहुत सारे मामले हैं जहां धारा कोड पुनरावृत्ति कोड से अधिक तेज़ है, मोटे तौर पर क्योंकि प्रति-तत्व पहुंच लागत स्ट्रीमर पुनरावृत्तियों की तुलना में धाराओं के साथ सस्ता है। और कई मामलों में, स्ट्रीम संस्करण उस चीज के लिए इनलाइन करता है जो हाथ से लिखे गए संस्करण के बराबर है। बेशक, शैतान विवरण में है; किसी भी दिए गए कोड में भिन्न व्यवहार हो सकता है।
ब्रायन गोएत्ज

26
@BrianGoetz, क्या आप उपयोग के मामलों को निर्दिष्ट कर सकते हैं, जब धाराएँ तेज होती हैं?
एलेक्जेंडर

1
FMH के अंतिम संस्करण में: @Benchmarkइसके बजाय का उपयोग करें@GenerateMicroBenchmark
pdem

3
@BrianGoetz, क्या आप ऐसे मामलों का उपयोग कर सकते हैं, जब धाराएँ तेज़ होती हैं?
किलटेक

17

1) आप बेंचमार्क का उपयोग करते हुए 1 सेकंड से कम समय देखते हैं। इसका मतलब है कि आपके परिणामों पर दुष्प्रभावों का मजबूत प्रभाव हो सकता है। इसलिए, मैंने आपके कार्य को 10 गुना बढ़ा दिया

    int max = 10_000_000;

और अपना बेंचमार्क चलाया। मेरे परिणाम:

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)

बिना संपादन ( int max = 1_000_000) परिणाम थे

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)

यह आपके परिणामों की तरह है: संग्रह की तुलना में धारा धीमी है। निष्कर्ष: स्ट्रीम इनिशियलाइज़ेशन / मान संचारित करने के लिए बहुत समय व्यतीत किया गया।

2) टास्क स्ट्रीम बढ़ने के बाद तेजी से (यह ठीक है), लेकिन समानांतर धारा बहुत धीमी रही। क्या गलत है? नोट: आपके पास collect(Collectors.toList())कमांड है। एकल संग्रह के लिए एकत्रित करना अनिवार्य रूप से समवर्ती निष्पादन के मामले में प्रदर्शन की अड़चन और ओवरहेड का परिचय देता है। जगह से ओवरहेड की सापेक्ष लागत का अनुमान लगाना संभव है

collecting to collection -> counting the element count

धाराओं के लिए यह किया जा सकता है collect(Collectors.counting())। मुझे परिणाम मिले:

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)

यह एक बड़े काम के लिए है! ( int max = 10000000) निष्कर्ष: संग्रह की वस्तुओं को एकत्रित करने में अधिकांश समय लगता है। सबसे धीमा हिस्सा सूची में जोड़ रहा है। BTW, सरल के ArrayListलिए प्रयोग किया जाता है Collectors.toList()


आपको इस परीक्षण को माइक्रोबेनमार्क करने की आवश्यकता है, जिसका अर्थ है कि इसे पहले बहुत बार गर्म किया जाना चाहिए, और फिर बहुत सारे बाघों और औसतन निष्पादित किया जाना चाहिए।
स्किवि

@ यकीन है कि, आप सही हैं, खासकर क्योंकि माप में एक बड़ा विचलन है। मैंने केवल बुनियादी जांच की है और सटीक होने का दिखावा नहीं करते हैं।
सर्गेई फेडोरोव

सर्वर मोड में JIT, 10k निष्पादन के बाद किक करता है। और फिर कोड को संकलित करने और उसे स्वैप करने में कुछ समय लगता है।
प्रिवेंटर

इस वाक्य के बारे में: " आपके पास collect(Collectors.toList())कमांड है, यानी ऐसी स्थिति हो सकती है जब आपको कई थ्रेड्स द्वारा एकल संग्रह को संबोधित करने की आवश्यकता होती है। " मुझे लगभग यकीन है कि समानांतर में कई अलग-अलग सूची उदाहरणों toListको एकत्र करता है । केवल संग्रह में अंतिम चरण के रूप में तत्वों को एक सूची में स्थानांतरित किया जाता है और फिर वापस आ जाता है। इसलिए ओवरहेडाइजेशन नहीं होना चाहिए। यही कारण है कि कलेक्टरों में एक आपूर्तिकर्ता, एक संचयकर्ता और एक कॉम्बिनर फ़ंक्शन दोनों होते हैं। (यह अन्य कारणों से धीमा हो सकता है, निश्चित रूप से।)
Lii

@ एलआईआई मुझे लगता है कि collectयहां कार्यान्वयन के बारे में भी यही तरीका है। लेकिन अंत में कई सूचियों को एक ही में विलय कर दिया जाना चाहिए, और ऐसा लगता है कि दिए गए उदाहरण में विलय सबसे भारी ऑपरेशन है।
सर्गेई फेडोरोव

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

मैं कोड को थोड़ा बदल देता हूं, अपने मैक बुक प्रो पर चला गया जिसमें 8 कोर हैं, मुझे एक उचित परिणाम मिला:

संग्रह: बीता समय: 1522036826 एनएस (1.522037 सेकंड)

धाराएँ: बीता हुआ समय: 4315833719 ns (4.315834 सेकंड)

समानांतर धाराएं: बीता हुआ समय: 261152901 एनएस (0.261153 सेकंड)


मुझे लगता है कि आपका परीक्षण उचित है, आपको बस एक मशीन की अधिक सीपीयू कोर की आवश्यकता है।
मेलॉन

3

आप जो करने की कोशिश कर रहे हैं, उसके लिए मैं नियमित रूप से जावा एपी का उपयोग नहीं करूंगा। बॉक्सिंग / अनबॉक्सिंग का एक टन चल रहा है, इसलिए एक बड़ा प्रदर्शन ओवरहेड है।

व्यक्तिगत रूप से मुझे लगता है कि डिज़ाइन किए गए बहुत सारे एपीआई बकवास हैं क्योंकि वे बहुत सारे ऑब्जेक्ट लिटर बनाते हैं।

डबल / इंट की एक आदिम सरणियों का उपयोग करने की कोशिश करें और इसे एकल थ्रेडेड करने की कोशिश करें और देखें कि प्रदर्शन क्या है।

पुनश्च: आप बेंचमार्क करने के लिए जेएमएच पर एक नज़र रखना चाह सकते हैं। यह जेवीएम को गर्म करने जैसे कुछ विशिष्ट नुकसान का ख्याल रखता है।


लिंक्डलिस्ट ArrayLists से भी बदतर हैं क्योंकि आपको सभी नोड ऑब्जेक्ट बनाने की आवश्यकता है। मॉड ऑपरेटर भी कुत्ता धीमा है। मेरा मानना ​​है कि 10/15 चक्रों की तरह कुछ + यह निर्देश पाइपलाइन को सूखा देता है। यदि आप 2 से बहुत तेज़ विभाजन करना चाहते हैं, तो बस नंबर 1 को दाईं ओर स्थानांतरित करें। ये मूल चालें हैं, लेकिन मुझे यकीन है कि चीजों को गति देने के लिए मोड उन्नत चालें हैं, लेकिन ये संभवतः अधिक समस्या विशिष्ट हैं।
pwentjer

मैं मुक्केबाजी से वाकिफ हूं। यह सिर्फ एक अनौपचारिक बेंचमार्क है। यह विचार संग्रह और धाराओं के परीक्षण दोनों में मुक्केबाजी / अनबॉक्सिंग के समान है।
मिस्टर स्मिथ

पहले मैं यह सुनिश्चित करूँगा कि यह गलती को मापे नहीं। वास्तविक बेंचमार्क करने से पहले बेंचमार्क को कुछ समय चलाने का प्रयास करें। फिर कम से कम आपके पास JVM वार्मअप है और कोड सही ढंग से JITTED है। इसके बिना, आप शायद गलत निष्कर्ष निकालते हैं।
pwentjer

ठीक है, मैं आपकी सलाह के बाद नए परिणाम पोस्ट करूंगा। मैंने JMH पर एक नज़र डाली है, लेकिन इसके लिए मावेन की आवश्यकता है और इसे कॉन्फ़िगर करने में कुछ समय लगता है। फिर भी धन्यवाद।
मिस्टर स्मिथ

मुझे लगता है कि "आप क्या करने की कोशिश कर रहे हैं," के संदर्भ में बेंचमार्क परीक्षणों के बारे में सोचने से बचना सबसे अच्छा है। यानी, आमतौर पर इस तरह के अभ्यासों को प्रदर्शन के लिए पर्याप्त सरल बना दिया जाता है, लेकिन इतना जटिल होता है कि वे जैसे दिखते हैं, वैसे ही सरल हो सकते हैं।
12
हमारी साइट का प्रयोग करके, आप स्वीकार करते हैं कि आपने हमारी Cookie Policy और निजता नीति को पढ़ और समझा लिया है।
Licensed under cc by-sa 3.0 with attribution required.