【问题标题】:Properly exploit parallelism when building a map of expensive keys?在构建昂贵键的映射时正确利用并行性?
【发布时间】:2019-12-24 16:29:05
【问题描述】:

我正在用 Haskell 编写 rainbow table 的玩具实现。主要数据结构是严格的Map h c,包含大量对,由随机值c生成:

import qualified Data.Map as M
import System.Random

table :: (RandomGen g, Random c) => Int -> g -> Map h c
table n = M.fromList . map (\c -> (chain c, c)) . take n . randoms

chain 的计算成本非常高。支配计算时间的部分是令人尴尬的并行,所以如果并行运行,我希望在内核数量上获得准线性加速。

但是,我希望将计算的对立即添加到表中,而不是累积在内存中的列表中。需要注意的是,可能会发生冲突,在这种情况下,应该尽快丢弃冗余链。堆分析证实了这种情况。

我从Control.Parallel.Strategies 中找到了parMap,并尝试将其应用于我的建表功能:

table n = M.fromList . parMap (evalTuple2 rseq rseq) (\c -> (chain c, c)) . take n . randoms

但是,使用-N 运行时,我最多只能使用 1.3 个核心。堆分析至少表明中间列表不驻留在内存中,但“-s”还报告创建了 0 个火花。我使用 parMap 怎么可能?这样做的正确方法是什么?

编辑chain 定义为:

chain :: (c -> h) -> [h -> c] -> c -> h
chain h = h . flip (foldl' (flip (.h)))

其中(c -> h)是目标哈希函数,从明文到哈希, [h -> c] 是减速器函数家族。我希望实现在ch 上保持通用,但对于基准测试我对两者都使用严格的字节串。

【问题讨论】:

  • 您使用的是哪个版本的parallel?足够旧的版本存在一些非常严重的问题。
  • @dfeuer parallel-3.2.2.0,很遗憾
  • Hrmmmm.... 你在使用什么-N 参数?
  • @dfeuer 我试过 -N4 和 -N8,两者都给出或多或少相同的结果(低于 2 个核心使用)
  • Checkout github.com/lehins/haskell-scheduler 我专门为这样的用例编写了它。

标签: haskell parallel-processing


【解决方案1】:

这是我想出的。让我知道基准测试的结果:

#!/usr/bin/env stack
{- stack --resolver lts-14.1 script --optimize
  --package scheduler
  --package containers
  --package random
  --package splitmix
  --package deepseq
-}
{-# LANGUAGE BangPatterns #-}

import Control.DeepSeq
import Control.Scheduler
import Data.Foldable as F
import Data.IORef
import Data.List (unfoldr)
import Data.Map.Strict as M
import System.Environment (getArgs)
import System.Random as R
import System.Random.SplitMix


-- for simplicity
chain :: Show a => a -> String
chain = show

makeTable :: Int -> SMGen -> (SMGen, M.Map String Int)
makeTable = go M.empty
  where go !acc i gen
          | i > 0 =
            let (c, gen') = R.random gen
            in go (M.insert (chain c) c acc) (i - 1) gen'
          | otherwise = (gen,  acc)

makeTablePar :: Int -> SMGen -> IO (M.Map String Int)
makeTablePar n0 gen0 = do
  let gens = unfoldr (Just . splitSMGen) gen0
  gensState <- initWorkerStates Par (\(WorkerId wid) -> newIORef (gens !! wid))
  tables <-
    withSchedulerWS gensState $ \scheduler -> do
      let k = numWorkers (unwrapSchedulerWS scheduler)
          (q, r) = n0 `quotRem` k
      forM_ ((if r == 0 then [] else [r]) ++ replicate k q) $ \n ->
        scheduleWorkState scheduler $ \genRef -> do
          gen <- readIORef genRef
          let (gen', table) = makeTable n gen
          writeIORef genRef gen'
          table `deepseq` pure table
  pure $ F.foldl' M.union M.empty tables

main :: IO ()
main = do
  [n] <- fmap read <$> getArgs
  gen <- initSMGen
  print =<< makeTablePar n gen

关于实施的几点说明:

  • 不要使用来自 random 的生成器,它非常慢,splitmix 快​​ 200 倍
  • makeTable 中,如果您希望立即丢弃重复的结果,则需要手动循环或展开。但是由于我们需要返回生成器,所以我选择了手动循环。
  • 为了最大程度地减少线程之间的同步,将为每个线程构建独立的映射,并在最后将生成的映射合并在一起时删除重复的映射。

【讨论】:

  • 我认为你可以在这里使用union 而不是merge。我喜欢merge(我写的),但是当它们应用时我倾向于更喜欢更简单的工具。一旦你进入differenceWith,是的,直接跳到merge 而不是学习一些奇怪的额外API 是有意义的。
  • 我会看看然后回复你,非常感谢你。尽管如此,如果不回退到IO 和显式并发就无法实现这一目标,那将是有点令人沮丧。
  • @dfeuer 我记得有一个像union 这样的函数,但不知怎的,它的名字让我忘记了,谢谢你的建议。在合并界面上做得很好,非常方便! @b0fh 欢迎您。不幸的是,如果你想从 Haskell 中的并行化中获得良好的性能,通常需要降低到 IO。在上述解决方案中,由于不确定性,您甚至不能假装它是使用 unsafePerformIO 进行的纯计算。
  • @b0fh,您绝对应该尝试monad-par,然后再一路下降到IO。但即使这样似乎也有点矫枉过正,IMO。如果问题是 PRNG 太慢,您可以使用更快的 PRNG 或递归拆分随机种子,以便将生成伪随机数的工作分配给需要它们的线程。或两者兼而有之。
  • @dfeuer 我强烈反对monad-par。事实上,它是纯粹的,并没有让它变得更好,尤其是因为它没有给你期望的性能改进。在此处查看基准:github.com/lehins/haskell-scheduler#benchmarks
猜你喜欢
  • 2021-12-25
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2018-02-08
  • 1970-01-01
  • 1970-01-01
  • 2014-05-09
  • 1970-01-01
相关资源
最近更新 更多