2017-01-12 16 views
17

Tôi biết rằng việc đưa ra các đánh giá về các tiêu chuẩn vi mô Java là cực kỳ nghiêm trọng, nhưng tôi thấy một cái gì đó có vẻ lạ lùng, và tôi muốn nhận được một số lời giải thích cho nó.Tại sao java lambda của tôi lại có chuyển nhượng giả nhanh hơn nhiều so với không có nó?

Lưu ý rằng tôi không sử dụng khung JMH cho việc này. Tôi biết điều đó, nhưng tôi không muốn đi theo chiều dài đó.

tôi sẽ cung cấp toàn bộ mẫu mã, nhưng trong ngắn hạn, khi tôi kiểm tra việc thực hiện hai phương pháp này

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

Tôi tìm thấy kết quả rất đáng ngạc nhiên. Trong mẫu mã lớn hơn, tôi đang đo bốn cách khác nhau để làm điều này, và ba cách đầu tiên rất gần với hiệu năng. Tất cả đều chạy khoảng 50 k ns mỗi lần lặp. Tuy nhiên, mẫu mã thứ hai luôn chạy dưới một nửa tổng số đó. Đúng rồi. Nó không chậm hơn, nó nhanh hơn một chút.

Việc chạy qua cho thấy số như thế này:

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

Mỗi lần chạy có số tương tự như này.

Bây giờ tôi sẽ cung cấp toàn bộ lớp và lớp cơ sở. Lưu ý rằng tôi có một "ấm lên" vượt qua, nơi tôi thực hiện các phương pháp được thử nghiệm một vài nghìn lần trước khi bắt đầu timings. Cũng lưu ý rằng mặc dù điều này chạy "testStreamToArray2" cuối cùng, tôi cũng đã thử di chuyển khối đó đến bài kiểm tra đầu tiên và các con số sẽ xuất hiện tương tự. Các dòng nhận xét ra có để thuyết phục tôi rằng các phương pháp đang thực sự làm một cái gì đó (thời gian vẫn còn về cùng với những dòng không bình luận ra).

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 + "]"; 
     } 
    } 
} 

Và lớp cơ sở:

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

Điều gì sẽ xảy ra nếu bạn chuyển 'testStreamToArray (fooList);' 'testStreamToArray2 (fooList);' trong '20000 iteration' của bạn? –

+0

Hoặc chỉ để chắc chắn, chạy thử nghiệm riêng biệt cho cả hai? Tự hỏi liệu dòng phụ có thể gây ra một số tối ưu hóa, có thể hiển thị trong bytecode không? – NickL

+0

Tôi đã thử chuyển đổi chúng trong sự hâm nóng, không có sự khác biệt. Tôi đã thử chạy với chỉ streamarray2, có kết quả tương tự. –

Trả lời

9

(. Sửa đổi câu trả lời)

Benchmarking trong Java là khó khăn. Tuy nhiên, chúng ta hãy ném JMH vào nó ... Tôi đã chuyển điểm chuẩn của bạn cho JMH (xem http://github.com/lemire/microbenchmarks).

Đây là những phương pháp có liên quan ...

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

Và đây là kết quả của chạy của tôi ...

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 

Nhưng kỳ lạ, dường như hai chức năng không chạy ở chính xác cùng tốc độ trung bình, có sự khác biệt khá đáng kể. Và đó là trong khi sử dụng JMH, một khuôn khổ khá tốt cho điểm chuẩn.

Tôi nghĩ lúc đầu rằng hai đoạn mã của bạn tương đương về mặt logic, nhưng chúng không phải là. Cách truy cập phương thức chiều dài vô dụng này buộc mã phải ném một ngoại lệ khi đối tượng String trả về là null.

Vì vậy, nó thực sự là gần gũi hơn với các đoạn mã sau đây ...

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

Và điều này thậm chí còn nhanh hơn so với chức năng tinh chỉnh của bạn ...

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 

Tại sao điều này có thể?

Chúng ta hãy để bên ngoài lập trình Java dòng 8 và viết các chức năng theo cách cũ ngớ ngẩn, có và không có kiểm tra null:

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

Và đó là cách chúng tôi có được hiệu suất tốt nhất ...

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 

Nhưng lợi ích của việc kiểm tra null vẫn còn.

Ok. Hãy để chúng tôi điểm chuẩn chỉ tổng số tiền, mà không cần bất kỳ điều gì khác (không có lớp tùy chỉnh). Hãy để chúng tôi có cả tổng tiêu chuẩn và tổng preceeded bởi một kiểm tra null:

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

Chúng tôi nhận được rằng việc kiểm tra null chậm chúng tôi xuống ...

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(). Length()' không có tác dụng phụ, trừ khi nó bị quá tải.Tôi biết rằng JIT có thể xử lý các công cụ như vậy, nhưng (ngoài chuỗi concatanation) javac thường không có tối ưu hóa, đặc biệt là không có gì phức tạp. Tôi đang thiếu gì? – maaartinus

+0

@maaartinus Tôi đã sửa lại câu trả lời của mình. Bạn đúng rồi. –

+0

@jtahlborn Ngoại trừ các chuỗi không bao giờ rỗng trong thử nghiệm thực tế. Vì vậy, bạn luôn làm chuỗi nối. –

0

Dựa trên câu trả lời của @ DanielLemire, tôi đã có một ý tưởng, có thể mang lại cho chúng tôi một chút nữa (không phải là một lời giải thích dứt khoát, nhưng quá dài cho một bình luận). Trong

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

những phần có liên quan

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

nơi tôi giới thiệu s cho kết quả của sự nối. Viết lại một chút, chúng tôi nhận được

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

Séc đầu tiên a == null làm cho séc thứ hai không cần thiết. javac dịch nối chuỗi bằng cách sử dụng StringBuilder. Điều này là đủ tốt cho người phiên dịch và được công nhận bởi trình biên dịch JIT, người cũng nhận ra sự kiểm tra thừa. Có rất nhiều vỏ bọc đặc biệt cho các mẫu thường được sử dụng nhất và không phải tất cả các mẫu đều được tối ưu hóa tốt như nhau. Tôi sẽ không ngạc nhiên nếu đó là nguyên nhân.

Một lý do khác có thể là mã ném NPE có thể dẫn đến một cái gì đó giống như

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

nơi mã máy sản xuất ngắn hơn đáng kể như việc xử lý đối với trường hợp rỗng được chuyển đi đến một số con đường đặc biệt. Trên thực tế, tất cả những gì cần thiết cho việc kiểm tra null là dereferencing con trỏ, được thực hiện anyway khi sao chép nội dung của a vào s. Khi đó là null, thì hệ thống bộ nhớ ảo tạo SIGSEGV, được xử lý ở đâu đó trên đường dẫn đặc biệt. Trên đường dẫn nhanh, không có gì ở tất cả. Cơ thể vòng lặp ngắn hơn và có thể được tối ưu hóa tốt hơn (ví dụ: bỏ vòng lặp nhiều hơn).

+0

Tôi không nghĩ rằng đó là chuỗi hợp nhất một mình, xem câu trả lời cập nhật của tôi. Khi tôi làm việc đơn giản với các mảng được xác định trước của chuỗi, và tôi kết hợp hai chuỗi tại một thời điểm, kiểm tra null làm chậm mọi thứ xuống –

+0

@DanielLemire Nói rằng nó sai đường là một cách nói. Có lẽ cơ hội duy nhất là nhìn vào [assembly được tạo ra] (https: // mechanical-sympathy) .blogspot.cz/2013/06/printing-generated-assembly-code-from.html) (Tôi đã không thử nó lâu, không có ý tưởng nếu nó vẫn hoạt động), nhưng tôi không có thời gian cho điều này bây giờ – maaartinus

+0

Tôi có một bãi chứa lắp ráp: https://github.com/lemire/microbenchmarks/tree/master/deepdive/Mysterious (xem amd64asm.txt). –

Các vấn đề liên quan