简体   繁体   English

我应该如何用溪流来总结一下?

[英]How should I sum something with streams?

I've seen and tried different implementations of how to sum something in a stream. 我已经看过并尝试过如何在流中对某些内容求和的不同实现。 Here is my code: 这是我的代码:

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

which gave me the following result : 这给了我以下结果:

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

I tried it several times and gave me different results each time (most of the time the last solution is the best), so I was wondering: 我尝试了几次并且每次给我不同的结果(大多数时候最后的解决方案是最好的),所以我想知道:

1) What is the correct way to do it? 1)正确的方法是什么?

2) Why? 2)为什么? (What is the difference to other solutions?) (与其他解决方案有什么区别?)

Before we get into the actual answer, a few things you should know: 在我们进入实际答案之前,您应该了解一些事项:

  1. The results of your test can vary quite strongly, depending on many factors (eg the computer you're running it on). 您的测试结果可能会有很大差异,具体取决于许多因素(例如您运行它的计算机)。 Here are the results of one run on my 8 core machine: 以下是我的8核机器上运行的结果:

     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 

    And then in a later run: 然后在以后的运行中:

     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 benchmarking isn't an easy topic. 微基准测试不是一个简单的主题。 There are methods to do it (and I'll get into some later) but just trying to use System.currentTimeMillies() won't work reliably in most cases. 有方法可以做到这一点(稍后我会介绍一些),但在大多数情况下,只是尝试使用System.currentTimeMillies()将无法可靠地工作。

  3. Just because Java 8 makes parallel operations easy, that doesn't mean that they should be used everywhere. 仅仅因为Java 8使并行操作变得容易,这并不意味着它们应该在任何地方使用。 Parallel operations make sense in some cases and don't in others. 并行操作在某些情况下有意义,在其他情况下则不然。

OK, now let's have a look at the various methods you're using. 好的,现在让我们来看看你正在使用的各种方法。

  • Sequential collectors: The summingInt collector you use has the following implementation: 顺序收集器:您使用的summingInt收集器具有以下实现:

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

    So, first of all a new array with one element will be created. 因此,首先将创建一个包含一个元素的新数组。 Then for every Person element in your stream the collect function will use the Person#getAge() function to retrieve the age as an Integer (not an int !) and add that age to the previous ones (in the 1D-array). 然后,对于流中的每个Person元素, collect函数将使用Person#getAge()函数将age作为Integer (而不是int !)检索,并将该age添加到之前的(在1D-array中)。 Finally, when the whole stream has been dealt with, it will extract the value from that array and return it. 最后,当处理完整个流时,它将从该数组中提取值并返回它。 So, there is a lot of auto-boxing and -unboxing going on here. 所以,这里有很多自动装箱和装箱。

  • Parallel collectors: This uses the ReferencePipeline#forEach(Consumer) function to accumulate the ages it gets from the mapping function. 并行收集器:它使用ReferencePipeline#forEach(Consumer)函数来累积从映射函数获得的年龄。 Again there is a lot of auto-boxing and -unboxing. 再次有很多自动装箱和-unboxing。
  • Sequential map and sum: Here you map your Stream<Person> to an IntStream . 顺序映射和求和:在此将Stream<Person>映射到IntStream One thing this means is that no auto-boxing or -unboxing is required any more; 这意味着一件事就是不再需要自动装箱或装箱了; this can in some cases save a lot of time. 在某些情况下,这可以节省大量时间。 Then it sums the resulting stream using the following implementation: 然后使用以下实现对结果流求和:

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

    The reduce function here will call ReduceOps#ReduceOp#evaluateSequential(PipelineHelper<T> helper, Spliterator<P_IN> spliterator) . 这里的reduce函数将调用ReduceOps#ReduceOp#evaluateSequential(PipelineHelper<T> helper, Spliterator<P_IN> spliterator) This will, in essence, use the Integer::sum function on all of your numbers, starting with 0 and the first number and then the result of that with the second number and so forth. 实质上,这将对所有数字使用Integer::sum函数,从0开始,第一个数字,然后是第二个数字的结果,依此类推。

  • Parallel map and sum: Here things get interesting. 并行映射和求和:这里的事情变得有趣。 It uses the same sum() function, however the reduce will in this case call ReduceOps#ReduceOp#evaluateParallel(PipelineHelper<T> helper, Spliterator<P_IN> spliterator) rather than the sequential option. 它使用相同的sum()函数,但是在这种情况下,reduce将调用ReduceOps#ReduceOp#evaluateParallel(PipelineHelper<T> helper, Spliterator<P_IN> spliterator)而不是顺序选项。 This will basically use a divide and conquer method to add up the values. 这将基本上使用分而治之的方法来累加值。 Now, the big advantage of divide and conquer is of course, that it can easily be done in parallel. 现在,分而治之的巨大优势当然是它可以很容易地并行完成。 However it does require splitting and re-joining the stream many times, which costs time. 但是,它确实需要多次拆分和重新连接流,这需要花费时间。 So how fast it is can vary quite heavily, depending on the complexity of the actual task it has to do with the elements. 因此它的速度变化很大,取决于它与元素有关的实际任务的复杂性。 In the case of adding, it's probably not worth it in most cases; 在添加的情况下,在大多数情况下可能不值得; as you can see from my results it was always one of the slower methods. 正如你从我的结果中看到的那样,它总是一种较慢的方法。

Now, to get a real idea of how long what takes, let's do a proper micro benchmark. 现在,为了真正了解所需的时间,让我们做一个适当的微观基准测试。 I'll be using JMH with the following benchmark code: 我将使用JMH以下基准代码:

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

}

The pom.xml for this maven project looks like this: 这个maven项目的pom.xml如下所示:

<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>

Make sure that Maven is running with Java 8 too, otherwise you'll get ugly errors. 确保Maven也在运行Java 8,否则你会遇到难看的错误。

I won't go into details about how to use JMH here (there are other places that do that) but here's the result I got: 我不会在这里详细介绍如何使用JMH(还有其他地方可以这样做),但这是我得到的结果:

# 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

So, on my system at the time I ran those tests, the sequential map sum was considerably faster, managing to do over 6100 operations in the time that the parallel map sum (using a divide and conquer method) managed to do only just over 2600. In fact, the sequential methods were both considerably faster than the parallel ones. 因此,在我运行这些测试的系统上,顺序映射总和相当快,在并行映射总和(使用分而治之的方法)设法仅执行超过2600时,管理超过6100次操作事实上,顺序方法都比并行方法快得多。

Now, in a situation which can more easily be run in parallel - eg where the Person#getAge() function was much more complex than just a getter - the parallel methods may well be a much better solution. 现在,在一个可以更容易并行运行的情况下 - 例如, Person#getAge()函数比只是一个getter复杂得多 - 并行方法可能是一个更好的解决方案。 In the end it all depends on the efficiency of parallel runs in the case being tested. 最后,这一切都取决于被测试案例中并行运行的效率。

Another thing to remember: if in doubt, do a proper micro benchmark. 另一件需要记住的事情是:如果有疑问,请做一个适当的微观基准。 ;-) ;-)

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM