Haskell 中的并行“任何”或“全部”

Posted

技术标签:

【中文标题】Haskell 中的并行“任何”或“全部”【英文标题】:Parallel "any" or "all" in Haskell 【发布时间】:2020-02-10 23:38:52 【问题描述】:

我现在多次遇到的一种模式是,需要通过在其上映射一些测试并查看是否有任何或所有元素通过来检查值列表。典型的解决方案就是使用方便的内置函数allany

问题是这些是串行评估的。在许多情况下,一旦 any 线程为 all 找到“假”或为 @ 找到“真”,在完成进程的同时进行并行评估会快得多 987654329@。我很确定使用 Control.Parallel 无法实现短路行为,因为它需要进程间通信,而且我还没有足够的 Control.Concurrent 来实现它。

这是数学中非常常见的模式(例如 Miller-Rabin Primality),所以我觉得有人可能已经为此提出了解决方案,但出于显而易见的原因,在谷歌搜索“并行或/和/任何/ all on list haskell" 不会返回很多相关结果。

【问题讨论】:

您可能会发现Parallel and Concurrent Programming in Haskell 很有用,尤其是章节2、3 和4。 这可以通过unamb 库实现 @luqui 迷人的;我会搞砸这个。如果我用这个写一个很好的平行所有/任何我会把它作为答案发布。 在尝试并行化任何东西之前,请考虑在 fork 一个新进程所需的时间内可以测试多少个条件。 @chepner 你在说什么?我们在这里不是在谈论 bash!我们可以使用线程进行并发和并行处理(无论是 C 中的 pthreads 还是 Haskell 中的绿色线程)您无需启动多个 Web 服务器来处理并发 Web 请求,而是在单个进程中运行多个线程!这同样适用于并行性。您可以启动与 CPU 一样多的线程并平均分配您的工作,从而处理 CPU 密集型任务。试试这个库来说服自己github.com/lehins/haskell-scheduler 【参考方案1】:

在许多实际程序中,您可以为此目的使用并行策略。这是因为,即使没有明确的机制来取消不需要的计算,这也会在垃圾收集器运行时隐式发生。作为一个具体的例子,考虑以下程序:

import Control.Concurrent
import Control.Parallel.Strategies
import Data.Int
import System.Mem

lcgs :: Int32 -> [Int32]
lcgs = iterate lcg
  where lcg x = 1664525 * x + 1013904223

hasWaldo :: Int32 -> Bool
hasWaldo x = waldo `elem` take 40000000 (lcgs x)

waldo :: Int32
waldo = 0

main :: IO ()
main = do
  print $ or (map hasWaldo [1..100] `using` parList rseq)

这使用并行列表策略在 100 个包含 4000 万个数字的 PRNG 流的输出中搜索 waldo = 0(永远不会找到)。编译运行:

ghc -threaded -O2 ParallelAny.hs
./ParallelAny +RTS -s -N4

它将四个核心固定大约 16 秒,最终打印出False。请注意,所有 100 个火花都已“转换”并运行到完成:

SPARKS: 100(100 converted, 0 overflowed, 0 dud, 0 GC'd, 0 fizzled)

现在,将waldo 更改为可以尽早找到的值:

waldo = 531186389   -- lcgs 5 !! 50000

并修改main 以使线程保持活动状态 10 秒:

main :: IO ()
main = do
  print $ or (map hasWaldo [1..100] `using` parList rseq)
  threadDelay 10000000

您会观察到它几乎立即打印出True,但 4 个内核仍保持在 100% CPU 上(至少在一段时间内),这说明不需要的计算继续运行并且没有短路,就像您一样可能会害怕。

但是,如果您在得到答案后强制进行垃圾回收,情况就会发生变化:

main :: IO ()
main = do
  print $ or (map hasWaldo [1..100] `using` parList rseq)
  performGC
  threadDelay 10000000

现在,您会看到 CPU 在打印 True 后不久就进入空闲状态,并且统计数据显示大部分计算在运行前被垃圾回收:

SPARKS: 100(9 converted, 0 overflowed, 0 dud, 91 GC'd, 0 fizzled)

在实际的程序中,不需要显式的performGC,因为理所当然地会定期执行 GC。一些不必要的计算会在找到答案后继续运行,但在许多现实场景中,不必要的计算比例并不是特别重要的因素。

特别是,如果列表很大并且列表元素的每个单独测试都很快,并行策略将具有出色的实际性能,并且易于实施。

【讨论】:

以上是关于Haskell 中的并行“任何”或“全部”的主要内容,如果未能解决你的问题,请参考以下文章

安全执行不受信任的 Haskell 代码

Haskell 中的单子——洪峰老师讲创客道(三十五)

Haskell 中的半显式并行

Haskell趣学指南

haskell中的并行映射

-bash: ghci: 找不到命令(Haskell 交互式 shell,Haskell 安装)