如何使ST中完成的计算并行运行?
我有一个向量需要通过随机访问填充,因此使用了ST,计算可以在单线程下正确运行,但无法弄清楚如何使用多个内核。
由于向量中索引的意义,需要随机访问。有n个事物,在n个事物中进行选择的每种可能方法在向量中都有一个条目,就像选择函数中一样。这些选择中的每一个都对应于一个二进制数(概念上,一个压缩的[Bool]),这些Int值是索引。如果有n个事物,则向量的大小为2^n。算法运行的自然方式是填充对应于“n选择1”的每个条目,然后填充“n选择2”的每个条目,对应于“n选择k”的条目取决于对应于“n选择(k-1)”的条目。不同选择的整数不以数字顺序出现,这就是需要随机访问的原因。
下面是一个无意义的(但是很慢的)计算,它遵循相同的模式。example
函数显示了我如何尝试将计算分解,以便在纯世界(没有ST monad)中完成大部分工作。在下面的代码中,bogus
是完成大部分工作的地方,目的是并行调用它,但只使用了一个内核。
import qualified Data.Vector as Vb
import qualified Data.Vector.Mutable as Vm
import qualified Data.Vector.Generic.Mutable as Vg
import qualified Data.Vector.Generic as Gg
import Control.Monad.ST as ST ( ST, runST )
import Data.Foldable(forM_)
import Data.Char(digitToInt)
main :: IO ()
main = do
putStrLn $ show (example 9)
example :: Int -> Vb.Vector Int
example n = runST $ do
m <- Vg.new (2^n) :: ST s (Vm.STVector s Int)
Vg.unsafeWrite m 0 (1)
forM_ [1..n] $ \i -> do
p <- prev m n (i-1)
let newEntries = (choiceList n i) :: [Int]
forM_ newEntries $ \e -> do
let v = bogus p e
Vg.unsafeWrite m e v
Gg.unsafeFreeze m
choiceList :: Int -> Int -> [Int]
choiceList _ 0 = [0]
choiceList n 1 = [ 2^k | k <- [0..(n-1) ] ]
choiceList n k
| n == k = [2^n - 1]
| otherwise = (choiceList (n-1) k) ++ (map ((2^(n-1)) +) $ choiceList (n-1) (k-1))
prev :: Vm.STVector s Int -> Int -> Int -> ST s Integer
prev m n 0 = return 1
prev m n i = do
let chs = choiceList n i
v <- mapM (\k -> Vg.unsafeRead m k ) chs
let e = map (\k -> toInteger k ) v
return (sum e)
bogus :: Integer -> Int -> Int
bogus prior index = do
let f = fac prior
let g = (f^index) :: Integer
let d = (map digitToInt (show g)) :: [Int]
let a = fromIntegral (head d)^2
a
fac :: Integer -> Integer
fac 0 = 1
fac n = n * fac (n - 1)
如果有人对此进行测试,那么在show (example 9)
中使用超过9或10将花费比您希望等待这样一个毫无意义的数字序列更长的时间。
3条答案
按热度按时间6uxekuva1#
只需要在
IO
中执行即可,如果您需要在纯代码中使用结果,那么unsafePerformIO
是可用的。下面的版本在
+RTS -N16
上运行的速度比+RTS -N1
快3 - 4倍,我的修改包括将ST
矢量转换为IO
,将forM_
转换为forConcurrently_
,并在let !v = bogus ...
上添加一个bang注解。完整代码:
5ssjco0h2#
我认为这样做是不安全的,在一般情况下,这似乎会打破Haskell的参照透明性。
如果我们可以在
ST s
中执行多线程计算,那么我们就可以产生两个线程来竞争同一个STRef s Bool
,假设一个线程正在写False
,另一个线程正在写True
。在我们使用
runST
进行计算之后,我们得到了一个Bool
类型的表达式,有时是False
,有时是True
,这应该是不可能的。如果你绝对确定你的并行化不会破坏引用透明性,你可以尝试使用
unsafeIOToST
这样的不安全原语来产生新线程,使用时要格外小心。也许有更安全的方法来实现类似的事情。除了
ST
之外,我们确实在Control.Parallel.Strategies
中提供了一些并行性。pdsfdshx3#
在Haskell中有很多种并行化的方法。通常它们都能提供相当的性能提升,但是有些方法比其他方法更好,这主要取决于需要并行化的问题。这个特殊的用例对我来说非常有趣,所以我决定研究几种方法。
方法
∮ ∮ ∮ ∮ ∮一个月一个月
我们使用的是一个装箱向量,因此我们可以利用惰性和内置的Spark池来实现并行化。
vector-strategies
包提供了一个非常简单的方法,它可以迭代任何不可变的装箱向量,并并行计算所有thunk。也可以将向量分成块,但事实证明1
的块大小是最佳的:parallel
parVector
在下面使用par
,需要对向量进行一次额外的迭代。在这种情况下,我们已经在对向量进行迭代,因此直接使用parallel
中的par
实际上更有意义。这将允许我们在继续使用ST
单子的同时并行执行计算:值得注意的是,与向量中的元素总数相比,向量中每个元素的计算成本都很高。这就是为什么使用
par
是一个非常好的解决方案。如果情况正好相反,即向量非常大,但元素的计算成本并不太高,则最好使用未装箱的向量,并将其切换到另一种并行化方法。一米十一秒
@K.A.Buhr.描述了另一种方法,从
ST
切换到IO
并使用async
:@chi提出的问题是有道理的,但是在这个特定的实现中,使用
unsafePerformIO
而不是runST
是安全的,因为并行化不会违反确定性计算的不变量,也就是说,我们可以保证,不管提供给example
函数的输入是什么,输出总是完全相同的。一米十七
绿色线程在Haskell是相当便宜的,但他们不是免费的。上面的
async
包的解决方案有一个小缺点:每次调用forConcurrently_
时,它将启动至少与newEntries
列表中的元素一样多的线程。最好启动与功能(-N
RTS选项)一样多的线程,并让它们完成所有工作。为此,我们可以使用scheduler
包,这是一个工作窃取调度程序:GHC中的Spark pool也使用了一个工作窃取调度器,它内置在RTS中,与上面的包在任何形状或形式上都无关,但思想非常相似:具有许多计算单元的少数线程。
基准
以下是在16核计算机上针对
example 7
(9
的值取秒级,这会给criterion
带来太多噪声)的所有方法进行的一些基准测试。我们只获得了大约5倍的加速比,因为算法的很大一部分本质上是顺序的,无法并行化。