【问题标题】:How Stream is more efficient?Stream 如何更高效?
【发布时间】:2016-05-11 12:25:22
【问题描述】:

我正在尝试消化Stream 包,似乎很难理解。

我正在阅读Stream 包文档,并且在某个时候我尝试实现它以边做边学。这是我读过的文字:

中间操作返回一个新流。他们总是很懒惰; 执行诸如 filter() 之类的中间操作实际上并不 执行任何过滤,而是创建一个新流,当 遍历,包含初始流中匹配的元素 给定谓词。管道源的遍历直到 执行管道的终端操作。

我非常了解他们提供了一个新的Stream,所以我的第一个问题是,是否在不经过繁重操作的情况下创建流?

现在,由于中间操作是lazy,终端操作是eager,而且流比旧的if-else 编程标准更高效,并且更具可读性。

延迟处理流可以显着提高效率;在一个 管道,例如上面的 filter-map-sum 示例,过滤,映射, 和求和可以融合到数据的单次传递中,用最少的 中间状态。懒惰还允许避免检查所有 不需要时的数据;对于诸如“找到第一个 超过 1000 个字符的字符串”,只需要检查 只需足够的字符串即可找到具有所需特征的字符串 无需检查源中可用的所有字符串。 (这 当输入流是无限的时,行为变得更加重要 而不仅仅是大。)

为了证明这一点,我开始实施一个小程序来理解这个概念。这是程序:

        List<String> stringList = new ArrayList<>();
        for (int i = 0; i < 10000; i++) {
            stringList.add("String" + i);
        }
        long   start  = System.currentTimeMillis();
        Stream stream = stringList.stream().filter(s -> s.contains("99"));
        long   midEnd = System.currentTimeMillis();
        System.out.println("Time is millis before applying terminal operation: " + (midEnd - start));
        System.out.println(stream.findFirst().get());
        long end = System.currentTimeMillis();
        System.out.println("Whole time in millis: " + (end - start));
        System.out.println("Time in millis for Terminal operation: " + (end - midEnd));

        start = System.currentTimeMillis();
        for (String ss1 : stringList) {
            if (ss1.contains("99")) {
                System.out.println(ss1);
                break;
            }
        }
        end = System.currentTimeMillis();
        System.out.println("Time in millis with old standard: " + (end - start));

我已经多次执行这个程序,每次都证明我从中间操作创建一个新的流是一项繁重的任务。与中间操作相比,终端操作确实需要很少的时间。

总的来说,旧的if-else 模式比streams 更有效。所以,这里还有更多问题:

  1. 我是不是误会了什么?
  2. 如果我理解正确,为什么以及何时使用流?
  3. 如果我做错或理解错了,请您帮助澄清我的概念Package java.util.stream

实际数字:

尝试 1:

Time is millis before applying terminal operation: 73
String99
Whole time in millis: 76
Time in millis for Terminal operation: 3
String99
Time in millis with old standard: 0

尝试 2:

Time is millis before applying terminal operation: 56
String99
Whole time in millis: 59
Time in millis for Terminal operation: 3
String99
Time in millis with old standard: 0

尝试 3:

Time is millis before applying terminal operation: 69
String99
Whole time in millis: 72
Time in millis for Terminal operation: 3
String99
Time in millis with old standard: 0

如果有帮助,这些是我的机器详细信息:

Memory: 11.6 GiB
Processor: Intel® Core™ i7-3632QM CPU @ 2.20GHz × 8 
OS-Type: 64-bit

【问题讨论】:

  • stringList.stream().filter(s -&gt; s.contains("99")); 可能需要不到一微秒的时间——你不能像现在这样测量它。必读:stackoverflow.com/questions/504103/…
  • imho :使用它来提高代码的可读性。此外,使用Stream 编写允许您轻松选择激活/停用并行计算,但它不会神奇地优化已经优化的代码..
  • 我仍然认为基准测试没有意义。我认为你可以在这里制定一个经验法则。 If your task takes less than 1ms to complete, using a stream will not improve the performance.
  • 您只是通过创建一个对象来增加一些开销。您有一项不可衡量的任务,并且您正在为其增加开销。您需要回到绘图板上进行基准测试。
  • 考虑非终结操作序列的一种方法类似于在将正则表达式重复应用于大量字符串之前“编译”正则表达式。是的,编译正则表达式需要几个周期,但是对编译进行基准测试没有意义——重要的是重复操作的性能。

标签: java java-8 java-stream


【解决方案1】:

Stream api 的基本原理之一是它消除了for 循环的固有假设,即所有迭代都以相同的方式发生。当您使用基于迭代器的for 循环时,您将迭代逻辑硬编码为始终按顺序迭代。考虑一下这个问题,“如果我想用更高效的方式更改 'for' 循环的实现怎么办?”

Stream api 解决了这个问题——它抽象了迭代的概念,并允许考虑处理多个数据点的其他方式——串行迭代与并行迭代,如果已知数据是无序的,则添加优化等.

考虑您的示例——尽管您无法更改 for 循环的实现,但您可以更改 Stream 的实现以适应不同的情况。例如,如果您要对每个任务执行更多 cpu 密集型操作,您可能会选择并行 Stream。下面是一个具有 10 毫秒延迟的示例,它模拟了更复杂的处理,并行完成,结果截然不同:

    List<String> stringList = new ArrayList<>();
    for (int i = 0; i < 10000; i++) {
        stringList.add("String" + i);
    }
    long   start  = System.currentTimeMillis();
    Stream stream = stringList.parallelStream().filter(s -> {
        try {
            Thread.sleep(10);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return s.contains("99" );});
    long   midEnd = System.currentTimeMillis();
    System.out.println("Time is millis before applying terminal operation: " + (midEnd - start));
    System.out.println(stream.findAny().get());
    long end = System.currentTimeMillis();
    System.out.println("Whole time in millis: " + (end - start));
    System.out.println("Time in millis for Terminal operation: " + (end - midEnd));

    start = System.currentTimeMillis();
    for (String ss1 : stringList) {
        try {
            Thread.sleep(20);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        if (ss1.contains("99")) {
            System.out.println(ss1);
            break;
        }
    }
    end = System.currentTimeMillis();
    System.out.println("Time in millis with old standard: " + (end - start));

我保留了每个人都在抱怨的相同基准逻辑,以便您进行比较。

如您所见,在某些情况下,for 循环总是比使用 Stream 更有效,但 Streams 在某些情况下也具有显着优势。从一个孤立的测试中推断出一种方法总是比另一种更好是不明智的——这也是生活的公理。

【讨论】:

  • 好吧,从您以及其他人的回答和我自己的经验来看,到目前为止我的理解是,流不能也不应该在迭代效率方面混淆?
  • 正确。也不应将其与低效率相混淆。
【解决方案2】:

除非您的测试涉及 JMH,否则您的代码几乎可以证明什么都没有,更糟糕的是,它会给人对现实的改变印象

assylias 发表的评论应该清楚说明出了什么问题。

您对“中间操作”和“短路”的测量也是错误的。中间操作,因为它是惰性的,实际上什么都不做,它只会在终端启动时发生。

如果您曾经使用过番石榴,那么在他们的代码中也是这样进行转换/过滤的,至少在逻辑上是这样。

【讨论】:

  • The intermediate operation, because it is lazy, does nothing really, it will only take place when a terminal one will kick in. 我同意。不,我从未使用过番石榴。
  • 虽然强调问题的不正确基准是正确的,但指出使用 JMH 对于正确的基准是强制性的,但这并没有更好。虽然 JMH 可以帮助避免某些基准测试错误,但它仍然只是一个工具。可能有些开发人员自己能够避免这些错误(毕竟,JMH 的作者也是开发人员),并且您在使用 JMH 时仍然可能犯错误(尤其是当您高估该工具可以为您做的事情时)。
  • @Holger 你可能是对的,从我通过 Caliper、JMH 和长循环进行的测试来看,JMH 在 IMO 方面做得最好。所以每次我听到这些是我的微基准测试结果时,我都会立即跳起来说使用 JMH。感觉很自然,可能有点不对劲。好点,谢谢
  • 推荐 JMH 并没有错,只是说“除非你的测试涉及 JMH,......[它不可能是正确的]”是不正确的。这太强了。我总是建议“使用 JMH”“仔细阅读它的文档”……
【解决方案3】:

正如其他人已经注意到您的基准测试存在缺陷一样。主要问题是忽略编译时间导致结果出现偏差。请尝试以下操作:

    Stream stream = stringList.stream().filter(s -> s.contains("99"));
    long   start  = System.currentTimeMillis();
    stream = stringList.stream().filter(s -> s.contains("99"));
    long   midEnd = System.currentTimeMillis();

现在支持filter 的代码已经编译,第二次调用很快。即使这样也行:

    Stream stream = stringList.stream().map(s -> s);
    long   start  = System.currentTimeMillis();
    stream = stringList.stream().filter(s -> s.contains("99"));
    long   midEnd = System.currentTimeMillis();

mapfilter 共享大部分代码,因此在这里调用filter 也很快,因为代码已经编译。如果您问:当然,在不同的流上调用 filtermap 也可以。

您的“旧式”代码不需要额外编译。

【讨论】:

  • 啊哈,现在我明白了。尽管我的基准有缺陷,但每个人都告诉我(我接受)。但你是唯一真正帮助我了解过滤器所需的编译时间的人。
  • 您仍在测量第二个 lambda 表达式的初始化时间。第二次执行代码会更快。或者,您可以将s -&gt; s.contains("99") 存储在Predicate 变量中并重新使用它。除此之外,始终使用System.nanoTime() 来测量经过的时间
  • 感谢@Holger 的宝贵反馈。
【解决方案4】:

我真的不相信你的“基准”,因为can go wrong的东西太多,你最好用framework。但无论如何,当人们或文档说它更有效时,他们并不是指您提供的示例。

作为提升集合的流(它们不保存数据)比像 Scala Lists 这样的渴望更有效,例如 filter 分配一个新列表,map 将结果转换为新的List.

当我们与此实现进行比较时,Streams 获胜。 但是,是的,流分配的对象在现代JVMs 上非常便宜,在现代GC's 上得到照顾。

【讨论】:

  • 您能否提供一个示例来说明这一点?
  • @java8.being 说明了什么?
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2020-08-31
  • 2020-08-24
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多