2017-01-12 7 views
17

मुझे पता है कि जावा माइक्रोबेंचमार्क के निर्णय करना बेहद भरा है, लेकिन मुझे ऐसा कुछ दिखाई दे रहा है जो अजीब लगता है, और मैं इसके लिए कुछ स्पष्टीकरण प्राप्त करना चाहता हूं।मेरा जावा लैम्ब्डा एक डमी असाइनमेंट के बिना इसके बिना बहुत तेज़ क्यों है?

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

मैं पूरी कोड नमूना प्रदान करेंगे, लेकिन संक्षेप में, मैं जब मैं बहुत ही आश्चर्य परिणामों को खोजने इन दोनों तरीकों

private FooPrime[] testStreamToArray(ArrayList<Foo> fooList) { 
    return (FooPrime[]) fooList.stream(). 
       map(it -> { 
        return new FooPrime().gamma(it.getAlpha() + it.getBeta()); 
       }). 
       toArray(FooPrime[]::new); 
} 

private FooPrime[] testStreamToArray2(ArrayList<Foo> fooList) { 
    return (FooPrime[]) fooList.stream(). 
       map(it -> { 
        int stuff = it.getAlpha().length(); 
        return new FooPrime().gamma(it.getAlpha() + it.getBeta()); 
       }). 
       toArray(FooPrime[]::new); 
} 

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

पिछले रन इस तरह संख्या पता चलता है:

manualcopy:54575 ns 
toarray:53617 ns 
streamtoarray:52990 ns 
streamtoarray2:24217 ns 

प्रत्येक रन इन के समान संख्या है।

अब मैं पूरी कक्षा और बेस क्लास प्रदान करूंगा। ध्यान दें कि मेरे पास "गर्मजोशी" पास है, जहां मैं समय शुरू करने से पहले कुछ हजार बार परीक्षण के तहत विधियों को निष्पादित करता हूं। यह भी ध्यान रखें कि यद्यपि यह "testStreamToArray2" चलाता है, फिर भी मैंने उस ब्लॉक को पहले परीक्षण में ले जाने की कोशिश की, और संख्याएं इसके बारे में बताती हैं। टिप्पणी की गई लाइनें मुझे यह समझाने के लिए हैं कि विधियां वास्तव में कुछ कर रही हैं (समय अभी भी उन पंक्तियों के समान हैं जिन पर टिप्पणी नहीं की गई है)।

package timings; 

import java.util.ArrayList; 
import java.util.HashSet; 
import java.util.List; 
import java.util.Set; 

public class ListToArrayOfPrimesTiming { 

    public static void main(String[] args) { 
     ListToArrayOfPrimesTiming tests = new ListToArrayOfPrimesTiming(args); 
     tests.go(); 
    } 

    public ListToArrayOfPrimesTiming(String[] args) { } 

    private void go() { 

     final ArrayList<Foo> fooList = new ArrayList<>(); 

     for (int ctr = 0; ctr < 1000; ++ ctr) { 
      fooList.add(new Foo().alpha("a" + ctr).beta("b" + ctr)); 
     } 

     for (int ctr = 0; ctr < 20000; ++ ctr) { 
      testManualCopy(fooList); 
      testToArray(fooList); 
      testStreamToArray(fooList); 
      testStreamToArray2(fooList); 
     } 

     int iters = 100000; 

//  Set<Integer> lengths = new HashSet<>(); 
//  Set<FooPrime> distinctFooPrimes = new HashSet<>(); 
//  lengths.clear(); 
//  distinctFooPrimes.clear(); 

     new TimingContainer(iters, "manualcopy", new TimingTest() { 
      @Override 
      public void run() { 
       FooPrime[] fooPrimeArray = testManualCopy(fooList); 
//    lengths.add(fooPrimeArray.length); 
//    distinctFooPrimes.add(fooPrimeArray[0]); 
      } 
     }).run(); 

//  System.out.println("lengths[" + lengths + "]"); 
//  lengths.clear(); 
//  System.out.println("distinctFooPrimes[" + distinctFooPrimes + "]"); 
//  distinctFooPrimes.clear(); 

     new TimingContainer(iters, "toarray", new TimingTest() { 
      @Override 
      public void run() { 
       FooPrime[] fooPrimeArray = testManualCopy(fooList); 
//    lengths.add(fooPrimeArray.length); 
//    distinctFooPrimes.add(fooPrimeArray[0]); 
      } 
     }).run(); 

//  System.out.println("lengths[" + lengths + "]"); 
//  lengths.clear(); 
//  System.out.println("distinctFooPrimes[" + distinctFooPrimes + "]"); 
//  distinctFooPrimes.clear(); 

     new TimingContainer(iters, "streamtoarray", new TimingTest() { 
      @Override 
      public void run() { 
       FooPrime[] fooPrimeArray = testStreamToArray(fooList); 
//    lengths.add(fooPrimeArray.length); 
//    distinctFooPrimes.add(fooPrimeArray[0]); 
      } 
     }).run(); 

//  System.out.println("lengths[" + lengths + "]"); 
//  lengths.clear(); 
//  System.out.println("distinctFooPrimes[" + distinctFooPrimes + "]"); 
//  distinctFooPrimes.clear(); 

     new TimingContainer(iters, "streamtoarray2", new TimingTest() { 
      @Override 
      public void run() { 
       FooPrime[] fooPrimeArray = testStreamToArray2(fooList); 
//    lengths.add(fooPrimeArray.length); 
//    distinctFooPrimes.add(fooPrimeArray[0]); 
      } 
     }).run(); 

//  System.out.println("lengths[" + lengths + "]"); 
//  lengths.clear(); 
//  System.out.println("distinctFooPrimes[" + distinctFooPrimes + "]"); 
//  distinctFooPrimes.clear(); 
    } 

    private FooPrime[] testManualCopy(ArrayList<Foo> fooList) { 
     FooPrime[] fooPrimeArray = new FooPrime[fooList.size()]; 
     int index = -1; 
     for (Foo foo: fooList) { 
      ++ index; 
      fooPrimeArray[index] = new FooPrime().gamma(foo.getAlpha() + foo.getBeta()); 
     } 
     return fooPrimeArray; 
    } 

    private FooPrime[] testToArray(ArrayList<Foo> fooList) { 
     List<FooPrime> fooPrimeList = new ArrayList<>(); 
     for (Foo foo: fooList) { 
      fooPrimeList.add(new FooPrime().gamma(foo.getAlpha() + foo.getBeta())); 
     } 
     return fooPrimeList.toArray(new FooPrime[fooList.size()]); 
    } 

    private FooPrime[] testStreamToArray(ArrayList<Foo> fooList) { 
     return (FooPrime[]) fooList.stream(). 
        map(it -> { 
         return new FooPrime().gamma(it.getAlpha() + it.getBeta()); 
        }). 
        toArray(FooPrime[]::new); 
    } 

    private FooPrime[] testStreamToArray2(ArrayList<Foo> fooList) { 
     return (FooPrime[]) fooList.stream(). 
        map(it -> { 
         int stuff = it.getAlpha().length(); 
         return new FooPrime().gamma(it.getAlpha() + it.getBeta()); 
        }). 
        toArray(FooPrime[]::new); 
    } 

    public static FooPrime fooToFooPrime(Foo foo) { 
     return new FooPrime().gamma(foo.getAlpha() + foo.getBeta()); 
    } 

    public static class Foo { 
     private String alpha; 
     private String beta; 

     public String getAlpha() { return alpha; } 
     public String getBeta() { return beta; } 

     public void setAlpha(String alpha) { this.alpha = alpha; } 
     public void setBeta(String beta) { this.beta = beta; } 

     public Foo alpha(String alpha) { this.alpha = alpha; return this; } 
     public Foo beta(String beta) { this.beta = beta; return this; } 
    } 

    public static class FooPrime { 
     private String gamma; 

     public String getGamma() { return gamma; } 

     public void setGamma(String gamma) { this.gamma = gamma; } 

     public FooPrime gamma(String gamma) { this.gamma = gamma; return this; } 

     @Override 
     public int hashCode() { 
      final int prime = 31; 
      int result = 1; 
      result = prime * result + ((gamma == null) ? 0 : gamma.hashCode()); 
      return result; 
     } 

     @Override 
     public boolean equals(Object obj) { 
      if (this == obj) 
       return true; 
      if (obj == null) 
       return false; 
      if (getClass() != obj.getClass()) 
       return false; 
      FooPrime other = (FooPrime) obj; 
      if (gamma == null) { 
       if (other.gamma != null) 
        return false; 
      } else if (!gamma.equals(other.gamma)) 
       return false; 
      return true; 
     } 

     @Override 
     public String toString() { 
      return "FooPrime [gamma=" + gamma + "]"; 
     } 
    } 
} 

और आधार वर्ग:

package timings; 

public class TimingContainer { 
    private int   iterations; 
    private String  label; 
    private TimingTest timingTest; 

    public TimingContainer(int iterations, String label, TimingTest timingTest) { 
     this.iterations = iterations; 
     this.label  = label; 
     this.timingTest = timingTest; 
    } 

    public void run() { 
     long startTime = System.nanoTime(); 
     for (int ctr = 0; ctr < iterations; ++ ctr) { 
      timingTest.randomize(); 
      timingTest.run(); 
     } 
     long endTime = System.nanoTime(); 
     long totalns = (endTime - startTime); 
     System.out.println(label + ":" + (totalns/iterations) + " ns"); 
    } 
} 
+0

यदि आप 'testStreamToArray (fooList) स्विच करते हैं तो क्या होता है; ' ' testStreamToArray2 (fooList); 'आपके' 20000 पुनरावृत्ति 'में? –

+0

या बस यह सुनिश्चित करने के लिए, अलग परीक्षण दोनों के लिए चलता है? आश्चर्य है कि अतिरिक्त लाइन कुछ अनुकूलन का कारण बन सकती है, शायद बाइटकोड में दिखाई दे सकती है? – NickL

+0

मैंने उन्हें गर्मजोशी में बदलने की कोशिश की, कोई फर्क नहीं पड़ता। मैंने केवल streamarray2 के साथ एक रन की कोशिश की, एक ही परिणाम मिला। –

उत्तर

9

(। संशोधित जवाब)

बेंचमार्किंग जावा में मुश्किल है। फिर भी, हम उस पर जेएमएच फेंक दें ... मैंने आपके बेंचमार्क को जेएमएच पर पोर्ट किया (http://github.com/lemire/microbenchmarks देखें)।

ये प्रासंगिक तरीके हैं ...

public FooPrime[] basicstream(BenchmarkState s) { 
      return (FooPrime[]) s.fooList.stream().map(it -> { 
        return new FooPrime().gamma(it.getAlpha() + it.getBeta()); 
      }).toArray(FooPrime[]::new); 
    } 

    public FooPrime[] tweakedbasicstream(BenchmarkState s) { 
      return (FooPrime[]) s.fooList.stream().map(it -> { 
        int stuff = it.getAlpha().length(); 
        return new FooPrime().gamma(it.getAlpha() + it.getBeta()); 
      }).toArray(FooPrime[]::new); 
    } 

और यहाँ, मेरी रन का परिणाम

git clone https://github.com/lemire/microbenchmarks.git 
cd microbenchmarks 
mvn clean install 
java -cp target/microbenchmarks-0.0.1-jar-with-dependencies.jar me.lemire.microbenchmarks.mysteries.MysteriousLambda 
Benchmark          Mode Samples  Score Error Units 
m.l.m.m.MysteriousLambda.basicstream   avgt  5 17013.784 ± 46.536 ns/op 
m.l.m.m.MysteriousLambda.tweakedbasicstream avgt  5 16240.451 ± 67.884 ns/op 

अजीब तरह है ... ऐसा लगता है कि दो कार्य वास्तव में नहीं चला वही औसत गति, एक काफी महत्वपूर्ण अंतर है। और वह जेएमएच का उपयोग करते समय, बेंचमार्किंग के लिए काफी अच्छा ढांचा है।

मैंने पहले सोचा था कि कोड के आपके दो टुकड़े तर्कसंगत समकक्ष थे, लेकिन वे नहीं हैं। स्पष्ट रूप से बेकार लंबाई विधि पहुंच कोड को एक अपवाद फेंकने के लिए मजबूर करती है जब स्ट्रिंग ऑब्जेक्ट लौटाया जाता है।

तो यह वास्तव में कोड का निम्न भाग के करीब है ...

@Benchmark 
    public FooPrime[] nullbasicstream(BenchmarkState s) { 
      return (FooPrime[]) s.fooList.stream().map(it -> { 
        if(it.getAlpha() == null) throw new NullPointerException(); 
        return new FooPrime().gamma(it.getAlpha() + it.getBeta()); 
      }).toArray(FooPrime[]::new); 
    } 

और यह भी तेजी से अपने फेरबदल समारोह से है ...

Benchmark          Mode Samples  Score Error Units 
m.l.m.m.MysteriousLambda.basicstream   avgt  5 17013.784 ± 46.536 ns/op 
m.l.m.m.MysteriousLambda.nullbasicstream  avgt  5 15983.762 ± 92.593 ns/op 
m.l.m.m.MysteriousLambda.tweakedbasicstream avgt  5 16240.451 ± 67.884 ns/op 

यह क्यों हो सकता है?

हमें जावा 8 की धारा प्रोग्रामिंग से बचने और समारोह मूर्खतापूर्ण पुराने तरीके से लिखते हैं, के साथ और बातिल की जांच के बिना करते हैं:

@Benchmark 
    public FooPrime[] basicsum(BenchmarkState s) { 
      int howmany = s.fooList.size(); 
      FooPrime[] answer = new FooPrime[s.fooList.size()]; 
      for(int k = 0; k < howmany ; ++k) { 
        Foo x = s.fooList.get(k); 
        answer[k] = new FooPrime(x.getAlpha() + x.getBeta()); 
      } 
      return answer; 
    } 

    @Benchmark 
    public FooPrime[] basicsumnull(BenchmarkState s) { 
      int howmany = s.fooList.size(); 
      FooPrime[] answer = new FooPrime[s.fooList.size()]; 
      for(int k = 0; k < howmany ; ++k) { 
        Foo x = s.fooList.get(k); 
        if(x.getAlpha() == null) throw new NullPointerException(); 
        answer[k] = new FooPrime(x.getAlpha() + x.getBeta()); 
      } 
      return answer; 
    } 

और हम सबसे अच्छा प्रदर्शन कैसे मिलता है ...

m.l.m.m.MysteriousLambda.basicstream      avgt  5 17019.730 ± 61.982 ns/op 
m.l.m.m.MysteriousLambda.nullbasicstream     avgt  5 16019.332 ± 62.831 ns/op 
m.l.m.m.MysteriousLambda.basicsum       avgt  5 15635.474 ± 119.890 ns/op 
m.l.m.m.MysteriousLambda.basicsumnull      avgt  5 14342.016 ± 109.958 ns/op 

लेकिन शून्य जांच का लाभ बनी हुई है।

ठीक है। आइए बेंचमार्क बस स्ट्रिंग रकम, बिना किसी और के (कोई कस्टम क्लास) बेंचमार्क करें। हम दोनों मानक योग और योग एक अशक्त चेक द्वारा preceeded करते हैं:

@Benchmark 
    public void stringsum(BenchmarkState s) { 
      for(int k = 0; k < s.N; ++k) s.list3[k] = s.list1[k] + s.list2[k]; 
    } 


    @Benchmark 
    public void stringsum_withexcept(BenchmarkState s) { 
      for(int k = 0; k < s.N; ++k) { 
        if(s.list1[k] == null) throw new NullPointerException(); 
        s.list3[k] = s.list1[k] + s.list2[k]; 
      } 
    } 

हम है कि रिक्त जांच हमें धीमा कर देती है नीचे लाने के लिए ...

m.l.m.m.StringMerge.stringsum    avgt  5 27011.111 ± 4.077 ns/op 
    m.l.m.m.StringMerge.stringsum_withexcept avgt  5 28387.825 ± 82.523 ns/op 
+0

'it.getAlpha() लंबाई() 'का कोई दुष्प्रभाव नहीं है, जब तक कि यह अधिभारित न हो जाए।मुझे पता है कि जेआईटी ऐसी चीजों को संभाल सकता है, लेकिन (स्ट्रिंग कॉन्सटेनेशन के अलावा) जावैक आमतौर पर कोई अनुकूलन नहीं करता है, विशेष रूप से कुछ भी जटिल नहीं होता है। मैं क्या खो रहा हूँ? – maaartinus

+0

@ मायार्टिनस मैंने अपना जवाब संशोधित किया। तुम सही हो। –

+0

@jtahlborn सिवाय इसके कि तार वास्तविक परीक्षण में कभी भी शून्य नहीं होते हैं। तो आप हमेशा स्ट्रिंग concatenation कर रहे हैं। –

0

@ DanielLemire के जवाब के आधार पर, मैं एक विचार है, जो हमें थोड़ा और आगे ला सकता है (एक निश्चित स्पष्टीकरण नहीं, लेकिन एक टिप्पणी के लिए बहुत लंबा)।

int stuff = it.getAlpha().length(); 
return new FooPrime().gamma(it.getAlpha() + it.getBeta()); 

में प्रासंगिक भागों

if (it.getAlpha() == null) throw new NullPointerException(); 
String s = it.getAlpha() + it.getBeta() 

जहाँ मैं संयोजन का परिणाम के लिए s शुरू की हैं। यह पुनर्लेखन थोड़ा हम मिल

String a = it.getAlpha(); 
if (a == null) throw new NullPointerException(); 
String b = it.getBeta(); 
String s = (a == null ? "null" : a) + (b == null ? "null" : b); 

पहला चेक a == null दूसरी जांच ज़रूरत से ज़्यादा बनाता है। javacStringBuilder का उपयोग करके स्ट्रिंग कॉन्सटेनेशन का अनुवाद करता है। यह दुभाषिया के लिए काफी अच्छा है और जेआईटी कंपाइलर द्वारा मान्यता प्राप्त है, जो अनावश्यक जांच को भी पहचानता है। सबसे अधिक इस्तेमाल किए जाने वाले पैटर्न के लिए बहुत से विशेष आवरण हैं और उनमें से सभी को समान रूप से अनुकूलित नहीं किया जाता है। अगर वह कारण था तो मुझे आश्चर्य नहीं होगा।

एक अन्य संभावित कारण है कि एनपीई फेंकने कोड की तरह

if (a == null) goto AWAY; 
String s = a + (b == null ? "null" : b); 

जहां उत्पादन मशीन कोड अशक्त मामले के लिए से निपटने के रूप में काफी हद तक कम कुछ असाधारण पथ के लिए दूर चले गए हो जाता है कुछ का कारण बन सकता है। असल में, शून्य जांच के लिए आवश्यक सभी चीजें पॉइंटर को डिफ्रेंस कर रही हैं, जो a की सामग्री को s में कॉपी करते समय भी किया जाता है। जब यह null है, तो वर्चुअल मेमोरी सिस्टम एक SIGSEGV उत्पन्न करता है, जो असाधारण पथ पर कहीं भी संभाला जाता है। तेज पथ पर, पर कुछ भी नहीं है। लूप बॉडी छोटा है और बेहतर अनुकूलित हो सकता है (उदा।, अधिक लूप अनोलिंग)।

+0

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

+0

@ डैनियललेमिर कह रहा है कि यह विकृत है एक अल्पसंख्यक है। संभवतः एकमात्र मौका [जेनरेटेड असेंबली] को देखने का है (https: // यांत्रिक-सहानुभूति .blogspot.cz/2013/06/मुद्रण-जेनरेट-असेंबली-कोड-से.html) (मैंने इसे लंबे समय तक नहीं देखा है, अगर यह अभी भी काम करता है तो कोई विचार नहीं है), लेकिन मेरे पास अब इसके लिए समय नहीं है – maaartinus

+0

मेरे पास एक असेंबली डंप है: https://github.com/lemire/microbenchmarks/tree/master/deepdive/Mysterious (amd64asm.txt देखें)। –

संबंधित मुद्दे