我有一个有限的值列表:
values :: [A]
......并且对这些价值观有一个昂贵但纯粹的功能:
expensiveFunction :: A -> Maybe B
如何并行地对每个值运行该函数,并仅返回以n
结束的第一个Just
结果并停止计算未完成的结果?
takeJustsPar :: (NFData b) => Int -> (a -> Maybe b) -> [a] -> [b]
takeJustsPar maxJusts f as = ???
我知道如何使用Control.Concurrent
执行此操作,但我想尝试使用Haskell的并行功能。此外,我能找到的(不足)文献似乎表明,Haskell的并行性特征使得产生并行计算和在多种功能之间调整工作量成本更低。
答案 0 :(得分:4)
我尝试了两种解决方案。第一个使用Par
monad(即Control.Monad.Par
):
import Control.Monad.Par (Par, NFData)
import Control.Monad.Par.Combinator (parMap)
import Data.Maybe (catMaybes)
import Data.List.Split (chunksOf)
takeJustsPar :: (NFData b) => Int -> Int -> (a -> Maybe b) -> [a] -> Par [b]
takeJustsPar n chunkSize f as = go n (chunksOf chunkSize as) where
go _ [] = return []
go 0 _ = return []
go numNeeded (chunk:chunks) = do
evaluatedChunk <- parMap f chunk
let results = catMaybes evaluatedChunk
numFound = length results
numRemaining = numNeeded - numFound
fmap (results ++) $ go numRemaining chunks
第二次尝试使用Control.Parallel.Strategies
:
import Control.Parallel.Strategies
import Data.List.Split (chunksOf)
chunkPar :: (NFData a) => Int -> Int -> [a] -> [a]
chunkPar innerSize outerSize as
= concat ((chunksOf innerSize as) `using` (parBuffer outerSize rdeepseq))
后者最终变得更容易组合,因为我可以写:
take n $ catMaybes $ chunkPar 1000 10 $ map expensiveFunction xs
...而不是将take
和catMaybes
行为融入并行策略。
后一种解决方案也提供了近乎完美的利用率。在我测试的令人尴尬的并行问题上,它为8个核心提供了99%的利用率。我没有测试Par
monad的使用情况,因为我正在借用同事的电脑,并且在我对Control.Parallel.Strategies
的表现感到满意时不想浪费时间。
所以答案是使用Control.Parallel.Strategies
,它提供了更多的可组合行为和更好的多核利用率。