繁体   English   中英

Java 分流器连续分流并列 Stream

[英]Java Spliterator Continually Splits Parallel Stream

我发现 Java 并行流有一些令人惊讶的行为。 我制作了自己的Spliterator ,生成的并行 stream 被分割,直到每个 stream 中只有一个元素。 这似乎太小了,我想知道我做错了什么。 我希望我可以设置一些特征来纠正这个问题。

这是我的测试代码。 这里的Float只是一个虚拟payload,我真正的stream class要复杂一些。

   public static void main( String[] args ) {
      TestingSpliterator splits = new TestingSpliterator( 10 );
      Stream<Float> test = StreamSupport.stream( splits, true );
      double total = test.mapToDouble( Float::doubleValue ).sum();
      System.out.println( "Total: " + total );
   }

此代码将不断拆分此 stream 直到每个Spliterator只有一个元素。 这似乎太高效了。

Output:

run:
Split on count: 10
Split on count: 5
Split on count: 3
Split on count: 5
Split on count: 2
Split on count: 2
Split on count: 3
Split on count: 2
Split on count: 2
Total: 5.164293184876442
BUILD SUCCESSFUL (total time: 0 seconds)

这是Spliterator的代码。 我主要关心的是我应该使用什么特性,但也许其他地方有问题?

public class TestingSpliterator implements Spliterator<Float> {
   int count;
   int splits;

   public TestingSpliterator( int count ) {
      this.count = count;
   }

   @Override
   public boolean tryAdvance( Consumer<? super Float> cnsmr ) {
      if( count > 0 ) {
         cnsmr.accept( (float)Math.random() );
         count--;
         return true;
      } else
         return false;
   }

   @Override
   public Spliterator<Float> trySplit() {
      System.err.println( "Split on count: " + count );
      if( count > 1 ) {
         splits++;
         int half = count / 2;
         TestingSpliterator newSplit = new TestingSpliterator( count - half );
         count = half;
         return newSplit;
      } else
         return null;
   }

   @Override
   public long estimateSize() {
      return count;
   }

   @Override
   public int characteristics() {
      return IMMUTABLE | SIZED;
   }
}

那么如何将 stream 分成更大的块呢? 我希望在 10,000 到 50,000 附近会更好。

我知道我可以从trySplit()方法返回null ,但这似乎是一种倒退的方式。 系统似乎应该对内核数量、当前负载以及使用 stream 的代码的复杂程度有一些概念,并相应地进行自我调整。 换句话说,我希望 stream 块大小由外部配置,而不是由 stream 本身在内部固定。

编辑:重新。 Holger 在下面的回答是,当我增加原始 stream 中的元素数量时,stream 的拆分会少一些,因此StreamSupport最终会停止拆分。

在初始 stream 大小为 100 个元素时, StreamSupport在达到 stream 大小为 2 时停止拆分(我在屏幕上看到的最后一行是Split on count: 4 )。

对于 1000 个元素的初始 stream 大小,各个 stream 块的最终大小约为 32 个元素。


编辑部分 deux:在查看了上面的 output 之后,我更改了代码以列出创建的各个Spliterator 以下是更改:

   public static void main( String[] args ) {
      TestingSpliterator splits = new TestingSpliterator( 100 );
      Stream<Float> test = StreamSupport.stream( splits, true );
      double total = test.mapToDouble( Float::doubleValue ).sum();
      System.out.println( "Total Spliterators: " + testers.size() );
      for( TestingSpliterator t : testers ) {
         System.out.println( "Splits: " + t.splits );
      }
   } 

对于TestingSpliterator的 ctor:

   static Queue<TestingSpliterator> testers = new ConcurrentLinkedQueue<>();

   public TestingSpliterator( int count ) {
      this.count = count;
      testers.add( this ); // OUCH! 'this' escape
   }

这段代码的结果是第一个Spliterator被拆分了 5 次。 下一个Spliterator被拆分 4 次。 下一组Spliterators被拆分 3 次。 等等。结果是制造了 36 个分离器,并且Spliterators被分成许多部分。 在典型的桌面系统上,这似乎是 API 认为最适合并行操作的方式。

我将在下面接受 Holger 的回答,这本质上是StreamSupport正在做正确的事情,别担心,开心就好。 对我来说,部分问题是我正在对非常小的 stream 尺寸进行早期测试,我对拆分的数量感到惊讶。 不要自己犯同样的错误。

你从错误的角度看它。 该实现没有拆分“直到每个拆分器都有一个元素”,而是拆分“直到有十个拆分器”。

单个拆分器实例只能由一个线程处理。 拆分器在开始遍历后不需要支持拆分。 因此,任何事先未使用的拆分机会都可能导致之后的并行处理能力受限。

请务必记住,Stream 实现收到了一个工作负载未知的ToDoubleFunction 在您的情况下,它不知道它与Float::doubleValue一样简单。 它可能是一个 function 需要一分钟来评估,然后每个 CPU 内核都有一个分离器是正确的。 即使拥有多个 CPU 内核也是一种有效的策略,可以处理某些评估花费的时间明显长于其他评估的可能性。

初始拆分器的典型数量将是“CPU 核心数”×4,尽管稍后当更多关于实际工作负载的知识存在时,这里可能会有更多拆分操作。 当您的输入数据少于该数字时,将其拆分直到每个拆分器留下一个元素就不足为奇了。

您可以尝试使用new TestingSpliterator( 10000 )1000100来查看拆分的数量不会发生显着变化,一旦实现假设有足够的块来保持所有 CPU 内核繁忙。

由于您的拆分器也不了解消耗 ZF7B44CFFAFD5C52223D5498196C8A2E7BZ 的每个元素的工作负载,因此您不必担心这一点。 如果您可以顺利地支持拆分为单个元素,那就这样做吧。

¹ 但是,对于没有链接操作的情况,它没有特殊的优化。

除非我遗漏了明显的内容,否则您始终可以在构造函数中传递一个bufferSize并将其用于您的trySplit

@Override
public Spliterator<Float> trySplit() {

     if( count > 1 ) {
        splits++;
        if(count > bufferSize) {
            count = count - bufferSize;
            return new TestingSpliterator( bufferSize, bufferSize);
        }

    }
    return null;
}

有了这个:

TestingSpliterator splits = new TestingSpliterator(12, 5);
Stream<Float> test = StreamSupport.stream(splits, true);

test.map(x -> new AbstractMap.SimpleEntry<>(
                   x.doubleValue(), 
                   Thread.currentThread().getName()))
    .collect(Collectors.groupingBy(
                Map.Entry::getValue, 
                Collectors.mapping(
                     Map.Entry::getKey, 
                     Collectors.toList())))
    .forEach((x, y) -> System.out.println("Thread : " + x + " processed : " + y));

您会看到有 3 个线程。 其中两个处理5元素,一个处理2

暂无
暂无

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

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