来源
作者:Doug Lea与Brian Goetz,Paul Sandoz,Alexey Shipilev,Heinz Kabutz,Joe Bowbeer,...合作
java.util.streams
框架包含对集合和其他数据源的数据驱动的操作。 大多数流方法对每个元素执行相同的操作。 使用parallelStream()
收集方法,如果您有多个内核,则可以将数据驱动转换为data-parallel 。 但是什么时候值得做?
考虑使用S.parallelStream().operation(F)
而不是S.stream().operation(F)
,前提是这些操作彼此独立并且在计算上昂贵或应用于大量有效拆分的元素(可拆分)数据结构,或两者兼而有之。 更准确地说:
F
:用于处理单个元素(通常是lambda)的函数是独立的,即 任何元素上的操作都是独立的,并且不会影响其他元素上的操作(有关使用无干扰无状态函数的建议,请参阅stream软件包的文档 )。S
:原始集合被有效分割。 除了集合之外,还有其他一些适合并行化的流数据源,例如java.util.SplittableRandom
(对于并行化,可以使用stream.parallel()
方法)。 但是大多数以I / O为核心的源主要是为顺序操作而设计的。- 顺序模式下的总运行时间超过了最小允许限制。 如今,对于大多数平台,该限制大约等于(在x10内)100微秒。 在这种情况下,不需要精确的测量。 出于实际目的,将
N
(元素数)简单地乘以Q
(一个F
的运算时间)就足够了, Q
可以近似地由运算数或代码行数来估计。 之后,您需要检查N * Q
至少小于10000
(如果害羞,请添加一个或几个零)。 因此,如果F
是x -> x + 1
类的小函数,则当N >= 10000
时,并行执行才有意义。 相反,如果F
是一个加权计算,类似于在国际象棋中找到下一个最佳移动,则Q
的值太大,可以忽略N
,但直到集合被完全拆分为止。
流处理框架不会(也不能)坚持上述任何条件。 如果计算是相互依存的,则并行执行是没有意义的,否则将完全有害并导致错误。 从上述工程问题和折衷中得出的其他标准包括:
- 启动
在大多数情况下,处理器中出现了其他内核,同时还增加了电源管理机制,这可能会导致内核启动速度变慢,有时还会附加JVM,操作系统和虚拟机管理程序。 在这种情况下,并行模式有意义的限制大致对应于开始处理具有足够数量核心的子任务所需的时间。 之后,并行计算比顺序计算更具能源效率(取决于处理器和系统的详细信息。有关示例,请参见本文 )。 - 细部化(粒度)
拆分小型计算几乎没有意义。 框架通常划分任务,以便各个部分可以在所有可用的系统核心上工作。 如果从一开始就几乎没有每个核心的工作,那么(通常是顺序的)组织并行计算的工作将被浪费。 考虑到实际上内核数的范围是2到256个阈值,因此它还可以防止过度分配任务的不良后果。 - 可除性
最有效的拆分集合包括ArrayList
和{Concurrent}HashMap
以及常规数组( T[]
,使用静态java.util.Arrays
方法将其拆分为多个部分)。 效率最低的拆分器是LinkedList
, BlockingQueue
和大多数基于I / O的源。 其余的位于中间(支持随机访问和/或有效搜索的数据结构通常被有效地拆分)。 如果拆分数据花费的时间比处理时间长,那么这是徒劳的。 如果Q
足够大,那么即使对于LinkedList
,由于并行化也会有所增加,但是这种情况很少见。 此外,某些来源无法拆分为一个元素,因此,问题分解的程度可能受到限制。
要获得这些效果的确切特性可能很困难(尽管可以尝试使用JMH之类的工具来完成 )。 但是累积效应很容易注意到。 自己感受一下-做一个实验。 例如,在32核测试机上,当您运行诸如ArrayList
上方的max()
或sum()
类的小功能时,收支平衡点约为10,000。 对于更多元素,最多可记录20倍加速。 少于10,000个项目的馆藏开放时间不少于10,000个,因此比顺序处理要慢。 最坏的结果发生在少于100个元素的情况下-在这种情况下,所涉及的线程会停止而没有做任何有用的事情,因为 计算在开始之前就已完成。 另一方面,当对元素的操作很耗时时,使用高效且完全可拆分的集合(例如ArrayList
,好处立即可见。
为了解释上述所有问题,在不合理的少量计算的情况下,使用parallel()
可能会花费大约100
微秒,否则使用该方法至少应节省自身时间(或者对于非常大的任务而言可能节省数小时)。 对于不同的平台,具体的成本和收益将随时间而变化,并且还取决于上下文。 例如,在一个连续的周期内并行运行小型计算可增强起伏的影响(发生这种情况的性能微测试可能无法反映实际情况)。
问与答
她可能会尝试,但是很多时候决定都是错误的。 在过去的30年中,对全自动多核并行性的追求并未导致一种通用的解决方案,因此,该框架使用了一种更可靠的方法,要求用户仅在是或否之间进行选择。 该选择基于顺序编程中经常遇到的工程问题,这些问题不可能完全消失。 例如,当您在包含单个元素的集合中查找最大值而直接使用该值(没有集合)进行比较时,可能会遇到一百倍的速度降低。 有时,JVM可以为您优化这种情况。 但这很少发生在顺序情况下,而在并行模式下则永远不会发生。 另一方面,我们可以期望,随着工具的开发,这些工具将帮助用户做出更好的决策。
- 如果为了做出正确的决定我对参数(
F
, N
, Q
, S
)没有足够的了解怎么办?
这也类似于顺序编程中遇到的问题。 例如,如果S
为HashSet
,则Collection
类的S.contains(x)
方法通常快速运行,如果LinkedList
为S
,则慢速运行,而在其他情况下为平均值。 通常,对于使用该集合的组件的创建者而言,摆脱这种情况的最佳方法是封装该组件并仅对其发布特定操作。 然后,用户将无需选择。 并行操作也是如此。 例如,具有内部价格集合的组件可以确定一种将其大小检查到极限的方法,这将是有意义的,直到按位计算过于昂贵为止。 一个例子:
public long getMaxPrice() { return priceStream().max(); } private Stream priceStream() { return (prices.size() < MIN_PAR) ? prices.stream() : prices.parallelStream(); }
这个想法可以扩展到关于何时以及如何使用并发的其他考虑。
一种极端情况是不满足独立性标准的功能,包括顺序I / O操作,对锁定同步资源的访问以及执行I / O的一个并行子任务中的错误影响其他情况的情况。 它们的并行化没有多大意义。 另一方面,有些计算偶尔会执行I / O或很少阻塞的同步(例如,大多数日志记录情况,以及使用ConcurrentHashMap
等竞争性集合)。 他们是无害的。 它们之间的关系需要更多的研究。 如果每个子任务可以在相当长的时间内等待I / O或访问而阻塞,则CPU资源将处于空闲状态,而程序或JVM可能不会使用它们。 由此对每个人都是不利的。 在这些情况下,并行流处理并非总是正确的选择。 但是也有不错的选择-例如,异步I / O和CompletableFuture
方法。
目前,使用JDK Stream
/ I / O生成器(例如BufferedReader.lines()
),它们主要适用于顺序模式,并在可用时逐一处理元素。 支持缓冲的I / O的高性能批量处理是可能的,但是,目前,这需要开发特殊的生成器Stream
, Spliterator
和Collector
。 将来的JDK版本中可能会添加对某些常见情况的支持。
- 如果我的程序在繁忙的计算机上运行并且所有内核都繁忙怎么办?
机器通常具有固定数量的内核,并且在执行并行操作时无法神奇地创建新内核。 但是,只要明确选择并行模式的标准就可以了 ,这是毫无疑问的。 您的并行任务将与其他CPU竞争,并且您会注意到加速度降低。 在大多数情况下,这仍然比其他方法更有效。 设计了底层机制,以便在没有可用内核的情况下,与顺序版本相比,您只会注意到稍微的减速,除非系统超载,以至于它花费所有时间来切换上下文而不是做一些实际工作,或者期望按顺序执行所有处理进行配置。 如果您有这样的系统,则管理员可能已经在JVM设置中禁用了多线程/核功能。 而且,如果您是系统管理员,则可以这样做。
是的 至少在某种程度上。 但是值得考虑的是,在选择如何进行操作时,流框架考虑了源和方法的限制。 通常,限制越少,并行化的可能性就越大。 另一方面,不能保证该框架将识别并应用所有可用的并发机会。 在某些情况下,如果您有时间和能力,那么您自己的解决方案可以更好地利用并行性的可能性。
如果您坚持这些技巧,那么通常就足够了。 可预测性并不是现代硬件和系统的强项,因此没有普遍的答案。 高速缓存位置,GC特性,JIT编译,内存访问冲突,数据位置,OS调度策略以及虚拟机监控程序的存在是产生重大影响的一些因素。 顺序模式的性能也受其影响,当使用并行性时,顺序模式的性能经常被放大:在顺序执行的情况下造成10%差异的问题可能导致并行处理产生10倍的差异。
流框架包括一些有助于增加加速机会的功能。 例如,对IntStream
这样的基元使用特殊化通常对并行模式的影响要大于对顺序模式的影响。 原因是在这种情况下,不仅资源(和内存)的消耗减少了,而且缓存的局部性也提高了。 在collect
操作的并行操作中,使用ConcurrentHashMap
代替HashMap
可以减少内部成本。 随着该框架的经验积累,将出现新的提示和技巧。
- 这一切太可怕了! 我们不能仅仅提出使用JVM属性关闭并发性的规则吗?
我们不想告诉您该怎么做。 程序员做错事的新方法的出现可能令人恐惧。 代码,体系结构和评估中的错误肯定会发生。 几十年前,有人预测应用程序级别的并发会导致巨大的灾难。 但这从来没有实现。