提出此问题已有7年了,似乎仍然没有人提出这个问题的好的解决方案。Repa没有类似mapM
/的traverse
功能,即使没有并行也可以运行。此外,考虑到最近几年取得的进步,似乎也不大可能实现。
由于Haskell中许多数组库的状态过时,以及我对它们的功能集的总体不满,我将几年的工作放在了一个数组库中massiv
,该库借鉴了Repa的一些概念,但将其带到了一个完全不同的水平。介绍足够了。
在此之前的今天,出现了像三种功能一元地图massiv
(不包括类似功能的代名词:imapM
,forM
。等):
mapM
-任意映射中的通常映射Monad
。由于明显的原因,无法并行化,并且也有点慢(沿着mapM
列表中的常规行比较慢)
traversePrim
-在这里,我们受限于PrimMonad
,它比快得多mapM
,但是这样做的原因对于本次讨论并不重要。
mapIO
-顾名思义,该限制仅限于IO
(或更确切地说MonadUnliftIO
,但这无关紧要)。因为我们在其中,所以IO
我们可以将数组自动拆分为与内核一样多的块,并使用单独的工作线程IO
在这些块中的每个元素上映射操作。与pure fmap
也可以并行化不同,IO
由于调度的不确定性以及映射操作的副作用,我们必须处于此状态。
因此,一旦我阅读了这个问题,我就以为自己可以在中解决该问题massiv
,但是速度并没有那么快。in mwc-random
和in 等随机数生成器random-fu
不能在多个线程中使用同一生成器。这意味着,我唯一缺少的难题是:“为产生的每个线程绘制一个新的随机种子并照常进行”。换句话说,我需要两件事:
- 该函数将初始化将有尽可能多的工作线程的生成器
- 以及一个抽象,它将根据动作在哪个线程中无缝地为映射函数提供正确的生成器。
这正是我所做的。
首先,我将使用特制的randomArrayWS
和initWorkerStates
函数给出示例,因为它们与问题更相关,然后再转到更通用的单子图。这是它们的类型签名:
randomArrayWS ::
(Mutable r ix e, MonadUnliftIO m, PrimMonad m)
=> WorkerStates g -- ^ Use `initWorkerStates` to initialize you per thread generators
-> Sz ix -- ^ Resulting size of the array
-> (g -> m e) -- ^ Generate the value using the per thread generator.
-> m (Array r ix e)
initWorkerStates :: MonadIO m => Comp -> (WorkerId -> m s) -> m (WorkerStates s)
对于不熟悉的人massiv
,该Comp
参数是要使用的计算策略,值得注意的构造函数是:
Seq
-按顺序运行计算,无需派生任何线程
Par
-旋转尽可能多的线程,并使用它们来完成工作。
mwc-random
最初,我将使用package作为示例,然后转到RVarT
:
λ> import Data.Massiv.Array
λ> import System.Random.MWC (createSystemRandom, uniformR)
λ> import System.Random.MWC.Distributions (standard)
λ> gens <- initWorkerStates Par (\_ -> createSystemRandom)
上面我们使用系统随机性为每个线程初始化了一个单独的生成器,但是我们也可以通过从WorkerId
参数(仅Int
是worker的索引)派生每个种子来使用唯一的种子。现在我们可以使用这些生成器来创建具有随机值的数组:
λ> randomArrayWS gens (Sz2 2 3) standard :: IO (Array P Ix2 Double)
Array P Par (Sz (2 :. 3))
[ [ -0.9066144845415213, 0.5264323240310042, -1.320943607597422 ]
, [ -0.6837929005619592, -0.3041255565826211, 6.53353089112833e-2 ]
]
通过使用Par
策略,scheduler
库会将生成工作平均分配给可用的工作程序,每个工作程序将使用其自己的生成器,从而使其线程安全。WorkerStates
只要没有同时执行,什么都不会阻止我们重复使用相同的任意次数,否则将导致异常:
λ> randomArrayWS gens (Sz1 10) (uniformR (0, 9)) :: IO (Array P Ix1 Int)
Array P Par (Sz1 10)
[ 3, 6, 1, 2, 1, 7, 6, 0, 8, 8 ]
现在mwc-random
放到一边,我们可以通过使用类似的功能将相同的概念重用于其他可能的用例generateArrayWS
:
generateArrayWS ::
(Mutable r ix e, MonadUnliftIO m, PrimMonad m)
=> WorkerStates s
-> Sz ix -- ^ size of new array
-> (ix -> s -> m e) -- ^ element generating action
-> m (Array r ix e)
和mapWS
:
mapWS ::
(Source r' ix a, Mutable r ix b, MonadUnliftIO m, PrimMonad m)
=> WorkerStates s
-> (a -> s -> m b) -- ^ Mapping action
-> Array r' ix a -- ^ Source array
-> m (Array r ix b)
下面是关于如何使用这个功能所承诺的例子rvar
,random-fu
和mersenne-random-pure64
图书馆。我们也可以在randomArrayWS
这里使用,但是为了举例说明,我们已经有一个带有不同RVarT
s 的数组,在这种情况下,我们需要一个mapWS
:
λ> import Data.Massiv.Array
λ> import Control.Scheduler (WorkerId(..), initWorkerStates)
λ> import Data.IORef
λ> import System.Random.Mersenne.Pure64 as MT
λ> import Data.RVar as RVar
λ> import Data.Random as Fu
λ> rvarArray = makeArrayR D Par (Sz2 3 9) (\ (i :. j) -> Fu.uniformT i j)
λ> mtState <- initWorkerStates Par (newIORef . MT.pureMT . fromIntegral . getWorkerId)
λ> mapWS mtState RVar.runRVarT rvarArray :: IO (Array P Ix2 Int)
Array P Par (Sz (3 :. 9))
[ [ 0, 1, 2, 2, 2, 4, 5, 0, 3 ]
, [ 1, 1, 1, 2, 3, 2, 6, 6, 2 ]
, [ 0, 1, 2, 3, 4, 4, 6, 7, 7 ]
]
需要注意的重要一点是,尽管在上面的示例中使用的是Mersenne Twister的纯实现,但我们无法逃脱IO。这是因为不确定的调度,这意味着我们永远不知道哪个工作人员将处理数组的哪个块,因此不知道哪个生成器将用于数组的哪个部分。从好的方面来说,如果生成器是纯的且可拆分的(例如)splitmix
,那么我们可以使用纯的,确定的和可并行化的生成函数:randomArray
,但这已经是一个独立的故事了。