为什么带副作用的过滤器比基于Spliterator的实现表现更好?

时间:2017-05-05 16:35:20

标签: java-8 java-stream

关于问题How to skip even lines of a Stream obtained from the Files.lines我遵循接受的答案方法,基于filterEven()接口实现我自己的Spliterator<T>方法,例如:

public static <T> Stream<T> filterEven(Stream<T> src) {
    Spliterator<T> iter = src.spliterator();
    AbstractSpliterator<T> res = new AbstractSpliterator<T>(Long.MAX_VALUE, Spliterator.ORDERED)
    {
        @Override
        public boolean tryAdvance(Consumer<? super T> action) {
            iter.tryAdvance(item -> {});    // discard
            return iter.tryAdvance(action); // use
        }
    };
    return StreamSupport.stream(res, false);
}

我可以通过以下方式使用:

Stream<DomainObject> res = Files.lines(src)
filterEven(res)
     .map(line -> toDomainObject(line))

然而,对于使用带有副作用的filter()的下一个方法测量此方法的性能,我注意到下一个方法表现更好:

final int[] counter = {0};
final Predicate<String> isEvenLine = item -> ++counter[0] % 2 == 0;
Stream<DomainObject> res = Files.lines(src)
     .filter(line -> isEvenLine ())
     .map(line -> toDomainObject(line))

我用JMH测试了性能,我没有在基准测试中包含文件负载。我之前将它加载到一个数组中。然后,每个基准测试首先从前一个数组创建Stream<String>,然后过滤偶数行,然后应用mapToInt()提取int字段的值,最后进行max()操作。这是基准测试之一(您可以查看整个Program here,这里有data file with about 186 lines):

@Benchmark
public int maxTempFilterEven(DataSource src){
    Stream<String> content = Arrays.stream(src.data)
            .filter(s-> s.charAt(0) != '#') // Filter comments
            .skip(1);                       // Skip line: Not available
    return filterEven(content)              // Filter daily info and skip hourly
            .mapToInt(line -> parseInt(line.substring(14, 16)))
            .max()
            .getAsInt();
}

我不明白为什么filter()方法比filterEven()(~50ops / ms)具有更好的性能(~80ops / ms)?

1 个答案:

答案 0 :(得分:4)

<强>简介

我想我知道原因,但不幸的是我不知道如何提高基于Spliterator的解决方案的性能(至少不重写整个Streams API功能)。

Sidenote 1 :在设计Stream API时,性能不是最重要的设计目标。如果性能至关重要,很可能在没有Stream API的情况下重写代码会使代码更快。 (例如,Stream API不可避免地增加了内存分配,从而增加了GC压力)。另一方面,在大多数情况下,Stream API以相对较小的性能降级为代价提供更好的更高级API。

部分 1 简短的理论答案

Stream旨在实现一种内部迭代,因为消费和外部迭代的主要手段(即基于Spliterator)是另一种模仿&#34;仿真&# 34 ;.因此,外部迭代涉及一些开销。懒惰增加了外部迭代效率的一些限制,并且需要支持flatMap使得在此过程中使用某种动态缓冲区成为必要。

Sidenote 2 在某些情况下,基于Spliterator的迭代可能与内部迭代一样快(在本例中为filter)。特别是在您从包含Spliterator的数据创建Stream的情况下。要查看它,您可以修改测试以将第一个过滤器实现为String数组:

String[] filteredData = Arrays.stream(src.data)
            .filter(s-> s.charAt(0) != '#') // Filter comments
            .skip(1)  
            .toArray(String[]::new);

然后比较已修改的maxTempFiltermaxTempFilterEven的效果,以接受预过滤的String[] filteredData。如果你想知道为什么会这样,你可能应该阅读这个长答案的其余部分或至少第2部分。

部分 2 更长的理论答案

Streams被设计为主要通过某些终端操作来消费。虽然支持逐个迭代元素但不是设计为消耗流的主要方式。

请注意使用&#34;功能&#34;流式API,例如mapflatMapfilterreducecollect您无法在某个步骤说出来#34;我有有足够的数据,停止迭代源和推动值#34;。您可以丢弃一些传入数据(如filter那样),但不能停止迭代。 (takeskip转换实际上是使用Spliterator内部实现的; anyMatchallMatchnoneMatchfindFirst,{ {1}}等使用非公开API findAny,因为无法进行多项终端操作,因此它们也更容易使用。如果管道中的所有转换都是同步的,您可以将它们组合成单个聚合函数(j.u.s.Sink.cancellationRequested)并在一个简单的循环中调用它(可选地将循环执行分成几个线程)。这就是我基于状态的过滤器的简化版本所代表的内容(请参阅 向我显示一些代码 部分中的代码)。如果管道中有Consumer但想法仍然相同,则会变得更复杂。

flatMap - 基于转换的根本不同,因为它为管道添加了异步的消费者驱动步骤。现在Spliterator而不是源Spliterator驱动迭代过程。如果您直接在源Stream上请求Spliterator,则可能会返回一些仅针对其内部数据结构进行迭代的实现,这就是为什么实现预过滤数据会消除性能差异的原因。但是,如果为某些非空管道创建Stream,除了要求源通过管道逐个推送元素直到某个元素通过所有过滤器之外,没有其他(简单)选择(另请参阅 向我显示一些代码 部分中的第二个示例)。源元素被逐个推送而不是在某些批次中被推动的事实是基本决定使Spliterator变得懒惰的结果。需要一个缓冲区而不是一个元素是支持Stream的结果:从源中推送一个元素可以为flatMap生成许多元素。

部分 3 向我显示一些代码

这部分试图为&#34;理论&#34;中描述的代码提供一些支持(代码链接到实际代码和模拟代码)。部分。

首先,您应该知道当前的Streams API实现将非终端(中间)操作累积到单个延迟管道中(请参阅j.u.s.AbstractPipeline及其子项,例如j.u.s.ReferencePipeline。然后,当应用终端操作,原始Spliterator中的所有元素都被#34;推送到#34;通过管道。

你看到的是两件事的结果:

  1. 流量管道在您遇到的情况下有所不同 有一个基于Stream的步骤。
  2. 您的Spliterator不是管道中的第一步
  3. 带有状态过滤器的代码或多或少类似于以下简单代码:

    OddLines

    请注意,这实际上是一个单循环,内部有一些计算(过滤/转换)。

    另一方面,当您将static int similarToFilter(String[] data) { final int[] counter = {0}; final Predicate<String> isEvenLine = item -> ++counter[0] % 2 == 0; int skip = 1; boolean reduceEmpty = true; int reduceState = 0; for (String outerEl : data) { if (outerEl.charAt(0) != '#') { if (skip > 0) skip--; else { if (isEvenLine.test(outerEl)) { int intEl = parseInt(outerEl.substring(14, 16)); if (reduceEmpty) { reduceState = intEl; reduceEmpty = false; } else { reduceState = Math.max(reduceState, intEl); } } } } } return reduceState; } 添加到管道中时,情况会发生显着变化,即使简化代码与实际发生的代码相似也会变得更大,例如:

    Spliterator

    这段代码更大,因为在循环内部没有一些非平凡的有状态回调的情况下,逻辑是不可能的(或者至少很难)。这里接口interface Sp<T> { public boolean tryAdvance(Consumer<? super T> action); } static class ArraySp<T> implements Sp<T> { private final T[] array; private int pos; public ArraySp(T[] array) { this.array = array; } @Override public boolean tryAdvance(Consumer<? super T> action) { if (pos < array.length) { action.accept(array[pos]); pos++; return true; } else { return false; } } } static class WrappingSp<T> implements Sp<T>, Consumer<T> { private final Sp<T> sourceSp; private final Predicate<T> filter; private final ArrayList<T> buffer = new ArrayList<T>(); private int pos; public WrappingSp(Sp<T> sourceSp, Predicate<T> filter) { this.sourceSp = sourceSp; this.filter = filter; } @Override public void accept(T t) { buffer.add(t); } @Override public boolean tryAdvance(Consumer<? super T> action) { while (true) { if (pos >= buffer.size()) { pos = 0; buffer.clear(); sourceSp.tryAdvance(this); } // failed to fill buffer if (buffer.size() == 0) return false; T nextElem = buffer.get(pos); pos++; if (filter.test(nextElem)) { action.accept(nextElem); return true; } } } } static class OddLineSp<T> implements Sp<T>, Consumer<T> { private Sp<T> sourceSp; public OddLineSp(Sp<T> sourceSp) { this.sourceSp = sourceSp; } @Override public boolean tryAdvance(Consumer<? super T> action) { if (sourceSp == null) return false; sourceSp.tryAdvance(this); if (!sourceSp.tryAdvance(action)) { sourceSp = null; } return true; } @Override public void accept(T t) { } } static class ReduceIntMax { boolean reduceEmpty = true; int reduceState = 0; public int getReduceState() { return reduceState; } public void accept(int t) { if (reduceEmpty) { reduceEmpty = false; reduceState = t; } else { reduceState = Math.max(reduceState, t); } } } static int similarToSpliterator(String[] data) { ArraySp<String> src = new ArraySp<>(data); int[] skip = new int[1]; skip[0] = 1; WrappingSp<String> firstFilter = new WrappingSp<String>(src, (s) -> { if (s.charAt(0) == '#') return false; if (skip[0] != 0) { skip[0]--; return false; } return true; }); OddLineSp<String> oddLines = new OddLineSp<>(firstFilter); final ReduceIntMax reduceIntMax = new ReduceIntMax(); while (oddLines.tryAdvance(s -> { int intValue = parseInt(s.substring(14, 16)); reduceIntMax.accept(intValue); })) ; // do nothing in the loop body return reduceIntMax.getReduceState(); } Spj.u.s.Stream接口的混合。

    • 班级j.u.Spliterator代表ArraySp的结果。

    • Arrays.stream类似于j.u.s.StreamSpliterators.WrappingSpliterator,它在实际代码中表示任何非空管道的WrappingSp接口的实现,即Spliterator with应用至少一个中间操作(参见j.u.s.AbstractPipeline.spliterator method)。在我的代码中,我将它与Stream子类合并,并将逻辑负责StatelessOp方法实现。另外,为简单起见,我使用filter实现了skip

    • filter对应于您的OddLineSp及其生成的OddLines

    • Stream代表ReduceIntMax <{1}}的{​​{1}}终端操作

    那么这个例子中重要的是什么?这里重要的是,由于您首先过滤原始流,因此ReduceOps是从非空管道(即Math.max)创建的。如果您仔细查看int,您会注意到每次调用OddLineSp时,它都会将调用委托给WrappingSp并将结果累积到WrappingSp中。一个tryAdvance。此外,由于管道中没有sourceSp,因此buffer的元素将逐个复制。即每次调用flatMap时,它都会调用buffer,只返回一个元素(通过回调),并将其进一步传递给调用者提供的WrappingSp.tryAdvance(除非元素没有&# 39; t匹配过滤器,在这种情况下,ArraySp.tryAdvance将被反复调用,但consumer仍然永远不会被多个元素填充。

    Sidenote 3 :如果您想查看真实代码,最有趣的地方是j.u.s.StreamSpliterators.WrappingSpliterator.tryAdvancej.u.s.StreamSpliterators.AbstractWrappingSpliterator.doAdvance反过来调用j.u.s.StreamSpliterators.AbstractWrappingSpliterator.fillBuffer,而j.u.s.StreamSpliterators.WrappingSpliterator.initPartialTraversalState又会调用{{3}}初始化的ArraySp.tryAdvance

    因此,影响性能的主要因素是复制到缓冲区。 不幸的是,对于我们这些普通的Java开发人员来说,Stream API的当前实现几乎已经关闭,您无法使用继承或组合来修改内部行为的某些方面。 您可以使用一些基于反射的黑客来使复制到缓冲区更有效地适应您的特定情况并获得一些性能(但牺牲了buffer的懒惰),但您无法完全避免这种复制,因此无论如何,基于pusher的代码会变慢。

    回到 Sidenote#2 中的示例,使用具体化Stream的基于Spliterator的测试工作得更快,因为没有{在Spliterator之前的管道中{1}},因此不会复制到中间缓冲区。

相关问题