2014-06-13 31 views
5

Tôi đã nhìn thấy và cố gắng hiện thực khác nhau như thế nào để tổng hợp một cái gì đó trong một dòng suối. Đây là mã của tôi:Tôi nên tính tổng hợp gì đó bằng luồng?

List<Person> persons = new ArrayList<Person>(); 

for(int i=0; i < 10000000; i++){ 
    persons.add(new Person("random", 26)); 
} 

Long start = System.currentTimeMillis(); 
int test = persons.stream().collect(Collectors.summingInt(p -> p.getAge())); 
Long end = System.currentTimeMillis(); 
System.out.println("Sum of ages = " + test + " and it took : " + (end - start) + " ms with collectors"); 

Long start3 = System.currentTimeMillis(); 
int test3 = persons.parallelStream().collect(Collectors.summingInt(p -> p.getAge())); 
Long end3 = System.currentTimeMillis(); 
System.out.println("Sum of ages = " + test3 + " and it took : " + (end3 - start3) + " ms with collectors and parallel stream"); 


Long start2 = System.currentTimeMillis(); 
int test2 = persons.stream().mapToInt(p -> p.getAge()).sum(); 
Long end2 = System.currentTimeMillis(); 
System.out.println("Sum of ages = " + test2 + " and it took : " + (end2 - start2) + " ms with map and sum"); 

Long start4 = System.currentTimeMillis(); 
int test4 = persons.parallelStream().mapToInt(p -> p.getAge()).sum(); 
Long end4 = System.currentTimeMillis(); 
System.out.println("Sum of ages = " + test4 + " and it took : " + (end4 - start4) + " ms with map and sum and parallel stream"); 

đó đã cho tôi kết quả sau:

Sum of ages = 220000000 and it took : 110 ms with collectors 
Sum of ages = 220000000 and it took : 272 ms with collectors and parallel stream 
Sum of ages = 220000000 and it took : 137 ms with map and sum 
Sum of ages = 220000000 and it took : 134 ms with map and sum and parallel stream 

Tôi đã thử nó nhiều lần và đã cho tôi kết quả khác nhau mỗi lần (hầu hết thời gian là giải pháp cuối cùng là tốt nhất) , vì vậy tôi đã tự hỏi:

1) Cách chính xác để làm điều đó là gì?

2) Tại sao? (Sự khác biệt với các giải pháp khác là gì?)

+9

Hãy cẩn thận với _Micro Benchmarks_ trong Java. Xem ví dụ: http://stackoverflow.com/questions/504103/how-do-i-write-a-correct-micro-benchmark-in-java – nosid

+0

parallelStream phụ thuộc vào số lượng CPU. Và luôn luôn ở cuối nó phải hợp nhất các kết quả - trong chuỗi đơn. –

Trả lời

10

Trước khi chúng tôi nhận được vào câu trả lời thực tế, một vài điều bạn nên biết:

  1. Kết quả thử nghiệm của bạn có thể thay đổi khá mạnh mẽ, tùy thuộc vào nhiều yếu tố (ví dụ: máy tính bạn đang chạy). Dưới đây là kết quả của một lần chạy thử trên máy tính 8 lõi của tôi:

    Sum of ages = 260000000 and it took : 94 ms with collectors 
    Sum of ages = 260000000 and it took : 61 ms with collectors and parallel stream 
    Sum of ages = 260000000 and it took : 70 ms with map and sum 
    Sum of ages = 260000000 and it took : 94 ms with map and sum and parallel stream 
    

    Và rồi trong một hoạt động sau:

    Sum of ages = 260000000 and it took : 68 ms with collectors 
    Sum of ages = 260000000 and it took : 67 ms with collectors and parallel stream 
    Sum of ages = 260000000 and it took : 66 ms with map and sum 
    Sum of ages = 260000000 and it took : 109 ms with map and sum and parallel stream 
    
  2. Micro điểm chuẩn không phải là một chủ đề dễ dàng. Có những phương pháp để làm điều đó (và tôi sẽ nhận được vào một số sau này) nhưng chỉ cố gắng sử dụng System.currentTimeMillies() sẽ không hoạt động đáng tin cậy trong hầu hết các trường hợp.

  3. Chỉ vì Java 8 làm cho hoạt động song song dễ dàng, điều đó không có nghĩa là chúng nên được sử dụng ở mọi nơi. Hoạt động song song có ý nghĩa trong một số trường hợp và không ở trong các trường hợp khác.

OK, bây giờ chúng ta hãy xem xét các phương pháp khác nhau mà bạn đang sử dụng.

  • nhà sưu tập Sequential: Các nhà sưu tập summingInt bạn sử dụng có thực hiện như sau:

    public static <T> Collector<T, ?, Integer> summingInt(ToIntFunction<? super T> mapper) { 
        return new CollectorImpl<>(
          () -> new int[1], 
          (a, t) -> { a[0] += mapper.applyAsInt(t); }, 
          (a, b) -> { a[0] += b[0]; return a; }, 
          a -> a[0], Collections.emptySet()); 
    } 
    

    Vì vậy, trước hết là một mảng mới với một phần tử sẽ được tạo ra. Sau đó, đối với mọi phần tử Person trong luồng của bạn, hàm collect sẽ sử dụng hàm Person#getAge() để truy lục tuổi là Integer (không phải là int!) Và thêm độ tuổi đó vào các phần trước đó (trong mảng 1D). Cuối cùng, khi toàn bộ luồng đã được xử lý, nó sẽ trích xuất giá trị từ mảng đó và trả về nó. Vì vậy, có rất nhiều auto-boxing và -unboxing đang diễn ra ở đây.

  • nhà sưu tập Parallel: này sử dụng ReferencePipeline#forEach(Consumer) chức năng để tích lũy các lứa tuổi nó nhận được từ các chức năng lập bản đồ. Một lần nữa có rất nhiều auto-boxing và -unboxing.
  • Bản đồ tuần tự và tổng: Đây là bản đồ số Stream<Person> của bạn đến số IntStream. Một điều này có nghĩa là không có auto-boxing hoặc -unboxing được yêu cầu nữa; điều này có thể trong một số trường hợp tiết kiệm rất nhiều thời gian.Sau đó, tổng hợp luồng kết quả bằng cách triển khai sau:

    @Override 
    public final int sum() { 
        return reduce(0, Integer::sum); 
    } 
    

    Chức năng reduce ở đây sẽ gọi ReduceOps#ReduceOp#evaluateSequential(PipelineHelper<T> helper, Spliterator<P_IN> spliterator). này sẽ, trong bản chất, sử dụng Integer::sum chức năng trên tất cả các con số của bạn, bắt đầu với 0 và số đầu tiên và sau đó là kết quả của việc đó với số thứ hai và vân vân.

  • đồ Parallel và sum: Ở đây mọi thứ trở nên thú vị. Nó sử dụng cùng chức năng sum(), tuy nhiên, mức giảm sẽ trong trường hợp này gọi là ReduceOps#ReduceOp#evaluateParallel(PipelineHelper<T> helper, Spliterator<P_IN> spliterator) thay vì tùy chọn tuần tự. Điều này về cơ bản sẽ sử dụng một phương pháp phân chia và chinh phục để thêm các giá trị. Bây giờ, lợi thế lớn của phân chia và chinh phục là tất nhiên, rằng nó có thể dễ dàng được thực hiện song song. Tuy nhiên, nó đòi hỏi phải tách và tái tham gia vào dòng suối nhiều lần, điều này sẽ tốn thời gian. Vì vậy, tốc độ của nó có thể thay đổi khá nhiều, tùy thuộc vào độ phức tạp của nhiệm vụ thực tế mà nó phải làm với các phần tử. Trong trường hợp thêm, nó có thể không có giá trị trong hầu hết các trường hợp; như bạn có thể thấy từ kết quả của tôi, nó luôn là một trong những phương pháp chậm hơn.

Bây giờ, để có được một ý tưởng thực sự về việc phải mất bao lâu, chúng ta hãy làm một điểm chuẩn thích hợp. Tôi sẽ sử dụng JMH với mã chuẩn sau:

package com.stackoverflow.user2352924; 

import org.openjdk.jmh.annotations.*; 

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

@BenchmarkMode(Mode.Throughput) 
@OutputTimeUnit(TimeUnit.MINUTES) 
@Warmup(iterations = 5, time = 5, timeUnit = TimeUnit.SECONDS) 
@Measurement(iterations = 10, time = 10, timeUnit = TimeUnit.SECONDS) 
@State(Scope.Benchmark) 
@Fork(1) 
@Threads(2) 
public class MicroBenchmark { 

    private static List<Person> persons = new ArrayList<>(); 

    private int test; 

    static { 
     for(int i=0; i < 10000000; i++){ 
      persons.add(new Person("random", 26)); 
     } 
    } 

    @Benchmark 
    public void sequentialCollectors() { 
     test = 0; 
     test += persons.stream().collect(Collectors.summingInt(p -> p.getAge())); 
    } 

    @Benchmark 
    public void parallelCollectors() { 
     test = 0; 
     test += persons.parallelStream().collect(Collectors.summingInt(p -> p.getAge())); 
    } 

    @Benchmark 
    public void sequentialMapSum() { 
     test = 0; 
     test += persons.stream().mapToInt(p -> p.getAge()).sum(); 
    } 

    @Benchmark 
    public void parallelMapSum() { 
     test = 0; 
     test += persons.parallelStream().mapToInt(p -> p.getAge()).sum(); 
    } 

} 

Các pom.xml cho dự án maven này trông như thế này:

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
     xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> 
    <modelVersion>4.0.0</modelVersion> 

    <groupId>com.stackoverflow.user2352924</groupId> 
    <artifactId>StackOverflow</artifactId> 
    <version>1.0</version> 
    <packaging>jar</packaging> 

    <name>Auto-generated JMH benchmark</name> 

    <prerequisites> 
     <maven>3.0</maven> 
    </prerequisites> 

    <dependencies> 
     <dependency> 
      <groupId>org.openjdk.jmh</groupId> 
      <artifactId>jmh-core</artifactId> 
      <version>${jmh.version}</version> 
     </dependency> 
     <dependency> 
      <groupId>org.openjdk.jmh</groupId> 
      <artifactId>jmh-generator-annprocess</artifactId> 
      <version>${jmh.version}</version> 
      <scope>provided</scope> 
     </dependency> 
    </dependencies> 

    <properties> 
     <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> 
     <jmh.version>0.9.5</jmh.version> 
     <javac.target>1.8</javac.target> 
     <uberjar.name>benchmarks</uberjar.name> 
    </properties> 

    <build> 
     <plugins> 
      <plugin> 
       <groupId>org.apache.maven.plugins</groupId> 
       <artifactId>maven-compiler-plugin</artifactId> 
       <version>3.1</version> 
       <configuration> 
        <compilerVersion>${javac.target}</compilerVersion> 
        <source>${javac.target}</source> 
        <target>${javac.target}</target> 
       </configuration> 
      </plugin> 
      <plugin> 
       <groupId>org.apache.maven.plugins</groupId> 
       <artifactId>maven-shade-plugin</artifactId> 
       <version>2.2</version> 
       <executions> 
        <execution> 
         <phase>package</phase> 
         <goals> 
          <goal>shade</goal> 
         </goals> 
         <configuration> 
          <finalName>microbenchmarks</finalName> 
          <transformers> 
           <transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer"> 
            <mainClass>org.openjdk.jmh.Main</mainClass> 
           </transformer> 
          </transformers> 
         </configuration> 
        </execution> 
       </executions> 
      </plugin> 
     </plugins> 
     <pluginManagement> 
      <plugins> 
       <plugin> 
        <artifactId>maven-clean-plugin</artifactId> 
        <version>2.5</version> 
       </plugin> 
       <plugin> 
        <artifactId>maven-deploy-plugin</artifactId> 
        <version>2.8.1</version> 
       </plugin> 
       <plugin> 
        <artifactId>maven-install-plugin</artifactId> 
        <version>2.5.1</version> 
       </plugin> 
       <plugin> 
        <artifactId>maven-jar-plugin</artifactId> 
        <version>2.4</version> 
       </plugin> 
       <plugin> 
        <artifactId>maven-javadoc-plugin</artifactId> 
        <version>2.9.1</version> 
       </plugin> 
       <plugin> 
        <artifactId>maven-resources-plugin</artifactId> 
        <version>2.6</version> 
       </plugin> 
       <plugin> 
        <artifactId>maven-site-plugin</artifactId> 
        <version>3.3</version> 
       </plugin> 
       <plugin> 
        <artifactId>maven-source-plugin</artifactId> 
        <version>2.2.1</version> 
       </plugin> 
       <plugin> 
        <artifactId>maven-surefire-plugin</artifactId> 
        <version>2.17</version> 
       </plugin> 
      </plugins> 
     </pluginManagement> 
    </build> 

</project> 

Hãy chắc chắn rằng Maven đang chạy với Java 8 quá, nếu không bạn' sẽ gặp lỗi xấu.

tôi sẽ không đi vào chi tiết về cách sử dụng JMH đây (có những nơi khác mà làm điều đó) nhưng đây là kết quả tôi nhận:

# Run complete. Total time: 00:08:48 

Benchmark          Mode Samples  Score Score error Units 
c.s.u.MicroBenchmark.parallelCollectors  thrpt  10 3658,949  775,115 ops/min 
c.s.u.MicroBenchmark.parallelMapSum   thrpt  10 2616,905  221,109 ops/min 
c.s.u.MicroBenchmark.sequentialCollectors thrpt  10 5502,160  439,024 ops/min 
c.s.u.MicroBenchmark.sequentialMapSum  thrpt  10 6120,162  609,232 ops/min 

Vì vậy, trên hệ thống của tôi vào thời điểm đó tôi chạy các bài kiểm tra đó, tổng số bản đồ tuần tự nhanh hơn đáng kể, quản lý để thực hiện trên 6100 hoạt động trong thời gian mà tổng bản đồ song song (sử dụng phương pháp phân chia và chinh phục) chỉ hoạt động hơn 2600. Thực tế, các phương pháp tuần tự đều đáng kể nhanh hơn so với những cái song song.

Bây giờ, trong một tình huống mà có thể dễ dàng được chạy song song - ví dụ trong đó hàm Person#getAge() phức tạp hơn nhiều so với chỉ là một trình thu thập dữ liệu - các phương thức song song cũng có thể là một giải pháp tốt hơn nhiều. Cuối cùng, tất cả phụ thuộc vào hiệu quả của các hoạt động song song trong trường hợp được kiểm tra.

Một điều cần nhớ: nếu nghi ngờ, làm một chuẩn mực vi thích hợp. ;-)

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