2014-04-19 11 views
40

मैं जावा 8 की धाराओं के साथ खेल रहा हूं और मुझे मिलने वाले प्रदर्शन परिणामों को समझ नहीं आ रहा है।जावा 8 की धाराएं: समानांतर धारा धीमी क्यों है?

मेरे पास 2 कोर सीपीयू (इंटेल i73520M), विंडोज 8 एक्स 64, और 64-बिट जावा 8 अपडेट 5 है। मैं स्ट्रिंग्स की धारा/समांतर धारा पर सरल मानचित्र कर रहा हूं और पाया कि समांतर संस्करण कुछ हद तक धीमा है।

मुझे इस कोड चलाएँ:

String[] array = new String[1000000]; 
Arrays.fill(array, "AbabagalamagA"); 

Stream<String> stream = Arrays.stream(array); 

long time1 = System.nanoTime(); 

List<String> list = stream.map((x) -> x.toLowerCase()).collect(Collectors.toList()); 

long time2 = System.nanoTime(); 

System.out.println((time2 - time1)/1000000f); 

... मैं कहीं 600 के आसपास इस संस्करण का परिणाम हो रही है, कि समानांतर धारा का उपयोग करता है:

String[] array = new String[1000000]; 
Arrays.fill(array, "AbabagalamagA"); 

Stream<String> stream = Arrays.stream(array).parallel(); 

long time1 = System.nanoTime(); 

List<String> list = stream.map((x) -> x.toLowerCase()).collect(Collectors.toList()); 

long time2 = System.nanoTime(); 


System.out.println((time2 - time1)/1000000f); 

... मुझे कुछ देता है लगभग 900.

इस तथ्य पर विचार करते हुए कि समांतर संस्करण तेज नहीं होना चाहिए, मेरे पास 2 CPU कोर हैं? क्या कोई मुझे संकेत दे सकता है कि समांतर संस्करण धीमा क्यों है?

उत्तर

94

समानांतर में यहां कई मुद्दे चल रहे हैं, जैसा कि थे।

पहला यह है कि समांतर में किसी समस्या को हल करने में हमेशा अनुक्रमिक रूप से ऐसा करने से अधिक वास्तविक कार्य करना शामिल होता है। ओवरहेड कई धागे के बीच काम को विभाजित करने और परिणामों में शामिल होने या विलय करने में शामिल है। छोटी तारों को कम-मामले में परिवर्तित करने जैसी समस्याएं इतनी छोटी हैं कि वे समानांतर विभाजित ओवरहेड द्वारा घिरे होने के खतरे में हैं।

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

छोटे, सिंथेटिक मानक के लिए, वर्कलोड अक्सर फेंकने वाले परिणामों की गणना करता है। जेआईटी कंपाइलर्स इसका पता लगाने और कोड को खत्म करने में काफी अच्छे हैं जो कहीं भी उपयोग किए जाने वाले नतीजों का उत्पादन नहीं करते हैं। यह शायद इस मामले में नहीं हो रहा है, लेकिन यदि आप अन्य सिंथेटिक वर्कलोड के साथ घूमते हैं, तो यह निश्चित रूप से हो सकता है। बेशक, यदि जेआईटी बेंचमार्क वर्कलोड को हटा देता है, तो यह बेंचमार्क बेकार प्रदान करता है।

मैं दृढ़ता से एक विकसित विकसित बेंचमार्किंग ढांचे का उपयोग करने की सलाह देता हूं जैसे JMH अपने आप में से एक हाथ से रोलिंग के बजाय। जेएमएच में इन्हें आम बेंचमार्किंग नुकसान से बचने में मदद करने के लिए सुविधाएं हैं, और इसे स्थापित करना और चलाने में बहुत आसान है।

package com.stackoverflow.questions; 

import java.util.Arrays; 
import java.util.List; 
import java.util.stream.Collectors; 
import java.util.concurrent.TimeUnit; 

import org.openjdk.jmh.annotations.*; 

public class SO23170832 { 
    @State(Scope.Benchmark) 
    public static class BenchmarkState { 
     static String[] array; 
     static { 
      array = new String[1000000]; 
      Arrays.fill(array, "AbabagalamagA"); 
     } 
    } 

    @GenerateMicroBenchmark 
    @OutputTimeUnit(TimeUnit.SECONDS) 
    public List<String> sequential(BenchmarkState state) { 
     return 
      Arrays.stream(state.array) 
        .map(x -> x.toLowerCase()) 
        .collect(Collectors.toList()); 
    } 

    @GenerateMicroBenchmark 
    @OutputTimeUnit(TimeUnit.SECONDS) 
    public List<String> parallel(BenchmarkState state) { 
     return 
      Arrays.stream(state.array) 
        .parallel() 
        .map(x -> x.toLowerCase()) 
        .collect(Collectors.toList()); 
    } 
} 

मैं इस का उपयोग करते हुए आदेश भागा:

java -jar dist/microbenchmarks.jar ".*SO23170832.*" -wi 5 -i 5 -f 1 

(। विकल्पों पाँच वार्मअप पुनरावृत्तियों, पांच बेंचमार्क पुनरावृत्तियों से संकेत मिलता है, और एक JVM काँटेदार) अपनी दौड़ के दौरान यहाँ अपने बेंचमार्क JMH उपयोग करने के लिए बदला न गया हो , जेएमएच बहुत सारे वर्बोज़ संदेशों को उत्सर्जित करता है, जिन्हें मैंने elided किया है। सारांश परिणाम निम्नानुसार हैं।

Benchmark      Mode Samples   Mean Mean error Units 
c.s.q.SO23170832.parallel  thrpt   5  4.600  5.995 ops/s 
c.s.q.SO23170832.sequential thrpt   5  1.500  1.727 ops/s 

ध्यान दें कि परिणाम प्रति सेकंड ऑप्स में हैं, इसलिए ऐसा लगता है कि समानांतर रन की तरह था के बारे में तीन बार अनुक्रमिक रन की तुलना में तेजी। लेकिन मेरी मशीन में केवल दो कोर हैं। हममम। और प्रति रन औसत त्रुटि वास्तव में रनटाइम से बड़ी है! WAT? यहां कुछ ख़राब चल रहा है।

यह हमें तीसरे अंक पर लाता है। वर्कलोड पर अधिक बारीकी से देखकर, हम देख सकते हैं कि यह प्रत्येक इनपुट के लिए एक नई स्ट्रिंग ऑब्जेक्ट आवंटित करता है, और यह परिणाम को एक सूची में भी एकत्र करता है, जिसमें बहुत से पुनर्वितरण और प्रतिलिपि शामिल होती है। मुझे लगता है कि इसका परिणाम कचरा संग्रह की उचित मात्रा में होगा। हम सक्षम जीसी संदेश के साथ बेंचमार्क का पुनर्प्रसारण द्वारा इस देख सकते हैं:

[GC (Allocation Failure) 512K->432K(130560K), 0.0024130 secs] 
[GC (Allocation Failure) 944K->520K(131072K), 0.0015740 secs] 
[GC (Allocation Failure) 1544K->777K(131072K), 0.0032490 secs] 
[GC (Allocation Failure) 1801K->1027K(132096K), 0.0023940 secs] 
# Run progress: 0.00% complete, ETA 00:00:20 
# VM invoker: /Users/src/jdk/jdk8-b132.jdk/Contents/Home/jre/bin/java 
# VM options: -verbose:gc 
# Fork: 1 of 1 
[GC (Allocation Failure) 512K->424K(130560K), 0.0015460 secs] 
[GC (Allocation Failure) 933K->552K(131072K), 0.0014050 secs] 
[GC (Allocation Failure) 1576K->850K(131072K), 0.0023050 secs] 
[GC (Allocation Failure) 3075K->1561K(132096K), 0.0045140 secs] 
[GC (Allocation Failure) 1874K->1059K(132096K), 0.0062330 secs] 
# Warmup: 5 iterations, 1 s each 
# Measurement: 5 iterations, 1 s each 
# Threads: 1 thread, will synchronize iterations 
# Benchmark mode: Throughput, ops/time 
# Benchmark: com.stackoverflow.questions.SO23170832.parallel 
# Warmup Iteration 1: [GC (Allocation Failure) 7014K->5445K(132096K), 0.0184680 secs] 
[GC (Allocation Failure) 7493K->6346K(135168K), 0.0068380 secs] 
[GC (Allocation Failure) 10442K->8663K(135168K), 0.0155600 secs] 
[GC (Allocation Failure) 12759K->11051K(139776K), 0.0148190 secs] 
[GC (Allocation Failure) 18219K->15067K(140800K), 0.0241780 secs] 
[GC (Allocation Failure) 22167K->19214K(145920K), 0.0208510 secs] 
[GC (Allocation Failure) 29454K->25065K(147456K), 0.0333080 secs] 
[GC (Allocation Failure) 35305K->30729K(153600K), 0.0376610 secs] 
[GC (Allocation Failure) 46089K->39406K(154624K), 0.0406060 secs] 
[GC (Allocation Failure) 54766K->48299K(164352K), 0.0550140 secs] 
[GC (Allocation Failure) 71851K->62725K(165376K), 0.0612780 secs] 
[GC (Allocation Failure) 86277K->74864K(184320K), 0.0649210 secs] 
[GC (Allocation Failure) 111216K->94203K(185856K), 0.0875710 secs] 
[GC (Allocation Failure) 130555K->114932K(199680K), 0.1030540 secs] 
[GC (Allocation Failure) 162548K->141952K(203264K), 0.1315720 secs] 
[Full GC (Ergonomics) 141952K->59696K(159232K), 0.5150890 secs] 
[GC (Allocation Failure) 105613K->85547K(184832K), 0.0738530 secs] 
1.183 ops/s 

नोट::

java -verbose:gc -jar dist/microbenchmarks.jar ".*SO23170832.*" -wi 5 -i 5 -f 1 

इस तरह परिणाम देता है लाइनों # के साथ शुरुआत सामान्य JMH उत्पादन लाइनें हैं। बाकी सभी जीसी संदेश हैं। यह पांच गर्मजोशी पुनरावृत्तियों में से पहला है, जो पांच बेंचमार्क पुनरावृत्तियों से पहले है। बाकी पुनरावृत्तियों के दौरान जीसी संदेश एक ही नस में जारी रहे। मुझे लगता है कि यह कहना सुरक्षित है कि मापा प्रदर्शन जीसी ओवरहेड का प्रभुत्व है और रिपोर्ट किए गए परिणामों पर विश्वास नहीं किया जाना चाहिए।

इस बिंदु पर यह स्पष्ट नहीं है कि क्या करना है। यह पूरी तरह से सिंथेटिक वर्कलोड है। यह स्पष्ट रूप से आवंटन और प्रतिलिपि की तुलना में वास्तविक काम करने में बहुत कम CPU समय शामिल करता है। यह कहना मुश्किल है कि आप वास्तव में यहां मापने की कोशिश कर रहे हैं। एक दृष्टिकोण एक अलग वर्कलोड के साथ आना होगा जो कुछ अर्थों में अधिक "वास्तविक" है। बेंचमार्क रन के दौरान जीसी से बचने के लिए एक और दृष्टिकोण ढेर और जीसी पैरामीटर को बदलना होगा।

+12

+1 बहुत गहन उत्तर और सही तरीके से चलाने के लिए एक अच्छा ट्यूटोरियल * और एक माइक्रो बेंचमार्क की व्याख्या *! – assylias

8

आपके डेटा को संसाधित करने के लिए कई धागे का उपयोग करने से कुछ प्रारंभिक सेटअप लागतें होती हैं, उदा। धागा पूल शुरू करना। ये लागत उन धागे का उपयोग करने से लाभ से अधिक हो सकती है, खासकर अगर रनटाइम पहले से ही काफी कम है। इसके अतिरिक्त, यदि विवाद है, उदा। अन्य धागे चल रहे हैं, पृष्ठभूमि प्रक्रियाएं, आदि, समांतर प्रसंस्करण का प्रदर्शन आगे घट सकता है।

यह समस्या समांतर प्रसंस्करण के लिए नई नहीं है। यह लेख जावा 8 parallel() के प्रकाश में कुछ जानकारी देता है और कुछ और बातों पर विचार करने के लिए: http://java.dzone.com/articles/think-twice-using-java-8

14

जब बेंचमार्क कर रही है, आप JIT कम्पाइलर पर ध्यान देना चाहिए, और उस समय व्यवहार को बदल सकते हैं, जब JIT किक में। यदि मैं आपके परीक्षण कार्यक्रम में एक गर्म-अप चरण जोड़ता हूं, तो समांतर संस्करण अनुक्रमिक संस्करण से थोड़ा तेज है। यहां परिणाम हैं:

Warmup... 
Benchmark... 
Run 0: sequential 0.12s - parallel 0.11s 
Run 1: sequential 0.13s - parallel 0.08s 
Run 2: sequential 0.15s - parallel 0.08s 
Run 3: sequential 0.12s - parallel 0.11s 
Run 4: sequential 0.13s - parallel 0.08s 

पूर्ण स्रोत कोड निम्नलिखित है, जिसका मैंने इस परीक्षण के लिए उपयोग किया है।

public static void main(String... args) { 
    String[] array = new String[1000000]; 
    Arrays.fill(array, "AbabagalamagA"); 
    System.out.println("Warmup..."); 
    for (int i = 0; i < 100; ++i) { 
     sequential(array); 
     parallel(array); 
    } 
    System.out.println("Benchmark..."); 
    for (int i = 0; i < 5; ++i) { 
     System.out.printf("Run %d: sequential %s - parallel %s\n", 
      i, 
      test(() -> sequential(array)), 
      test(() -> parallel(array))); 
    } 
} 
private static void sequential(String[] array) { 
    Arrays.stream(array).map(String::toLowerCase).collect(Collectors.toList()); 
} 
private static void parallel(String[] array) { 
    Arrays.stream(array).parallel().map(String::toLowerCase).collect(Collectors.toList()); 
} 
private static String test(Runnable runnable) { 
    long start = System.currentTimeMillis(); 
    runnable.run(); 
    long elapsed = System.currentTimeMillis() - start; 
    return String.format("%4.2fs", elapsed/1000.0); 
} 
संबंधित मुद्दे