簡體   English   中英

在 ST 中並行計算可變向量

[英]Parallelize computation of mutable vector in ST

如何使在 ST 中完成的計算並行運行?

我有一個需要通過隨機訪問填充的向量,因此使用了 ST,並且計算在單線程下正確運行,但一直無法弄清楚如何使用多個內核。

由於向量中索引的含義,需要隨機訪問。 有 n 個事物,在 n 個事物中每一種可能的選擇方式在向量中都有一個條目,例如選擇 function。這些選擇中的每一個都對應一個二進制數(概念上,一個壓縮的 [Bool]),這些 Int 值是指數。 如果有n個東西,那么vector的大小就是2^n。 算法運行的自然方式是填寫對應於“n choose 1”的每個條目,然后填寫“n choose 2”的每個條目,等等。對應於“n choose k”的條目取決於對應於“ n 選擇 (k-1)。” 不同選擇的整數不按數字順序出現,這就是需要隨機訪問的原因。

這是遵循相同模式的無意義(但緩慢)的計算。 example function 展示了我如何嘗試分解計算,以便大部分工作在一個純粹的世界中完成(沒有 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 的時間將比您等待這種毫無意義的數字序列所花費的時間長得多。

我認為這不能以安全的方式完成。 在一般情況下,它似乎會破壞 Haskell 的引用透明性。

如果我們可以在ST s中執行多線程計算,那么我們可以生成兩個線程,它們在相同STRef s Bool上競爭。 假設一個線程正在編寫False而另一個線程正在編寫True

在我們對計算使用runST之后,我們得到一個Bool類型的表達式,它有時是False有時是True 那應該是不可能的。

如果您絕對確定您的並行化不會破壞引用透明性,您可以嘗試使用不安全的原語(如unsafeIOToST來生成新線程。 使用時要格外小心。

可能有更安全的方法來實現類似的目標。 ST之外,我們確實在Control.Parallel.Strategies中提供了一些並行性。

只需在IO中執行即可。 如果您需要在純代碼中使用結果,則可以使用unsafePerformIO

以下版本使用+RTS -N16時的運行速度比+RTS -N1快大約 3-4 倍。 我的更改涉及將ST向量轉換為IO ,將 forM_ 更改為forM_ ,並向forConcurrently_ let.v = bogus...添加 bang 注釋。

完整代碼:

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)
import Control.Concurrent.Async
import System.IO.Unsafe

main :: IO ()
main = do
  let m = unsafePerformIO (example 9)
  putStrLn $ show m

example :: Int -> IO (Vb.Vector Int)
example n = do
  m <- Vg.new (2^n)

  Vg.unsafeWrite m 0 (1)

  forM_ [1..n] $ \i -> do
    p <- prev m n (i-1)
    let newEntries = (choiceList n i) :: [Int]
    forConcurrently_ 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.IOVector Int -> Int -> Int -> IO 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)

在 Haskell 中有很多方法可以進行並行化。通常它們會提供相當的性能改進,但是有些比其他的更好,這主要取決於需要並行化的問題。 這個特殊的用例對我來說非常有趣,所以我決定研究一些方法。

方法

vector-strategies

我們使用的是盒裝向量,因此我們可以利用惰性和內置火花池進行並行化。 vector-strategies package 提供了一種非常簡單的方法,它可以迭代任何不可變的盒裝向量並並行評估所有 thunk。 也可以將向量分成塊,但事實證明塊大小為1是最佳的:

exampleParVector :: Int -> Vb.Vector Int
exampleParVector n = example n `using` parVector 1

parallel

parVector在下面使用par並且需要對向量進行一次額外的迭代。 在這種情況下,我們已經迭代了你的向量,因此直接從parallel中使用par實際上更有意義。 這將允許我們在繼續使用ST monad 的同時並行執行計算:

import Control.Parallel (par)
...

  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
      v `par` Vg.unsafeWrite m e v

重要的是要注意,與向量中的元素總數相比,向量中每個元素的計算都非常昂貴。 這就是為什么在這里使用par是一個非常好的解決方案。 如果情況相反,即向量非常大,但元素的計算成本並不高,則最好使用未裝箱的向量並將其切換為不同的並行化方法。

async

@KABuhr 描述了另一種方式。 ST切換到IO並使用async

import Control.Concurrent.Async (forConcurrently_)
...

  forM_ [1..n] $ \i -> do
    p <- prev m n (i-1)
    let newEntries = choiceList n i :: [Int]
    forConcurrently_ newEntries $ \e -> do
      let !v = bogus p e
      Vg.unsafeWrite m e v

@chi 提出的擔憂是有效的,但是在這個特定的實現中,使用unsafePerformIO而不是runST是安全的,因為並行化不違反確定性計算的不變性。 即,我們可以 promise 無論提供給example function 的輸入如何,output 將始終完全相同。

scheduler

Haskell 的綠線很便宜,但不是免費的。 上面使用async package 的解決方案有一個小缺點:每次調用forConcurrently_時,它至少會啟動與newEntries列表中的元素一樣多的線程。 最好啟動盡可能多的線程,因為有能力( -N RTS 選項)並讓它們完成所有工作。 為此,我們可以使用scheduler package,這是一個工作竊取調度程序:

import Control.Scheduler (Comp(Par), runBatch_, withScheduler_)
...

  withScheduler_ Par $ \scheduler ->
    forM_ [1..n] $ \i -> runBatch_ scheduler $ \_ -> do
      p <- prev m n (i-1)
      let newEntries = choiceList n i :: [Int]
      forM_ newEntries $ \e -> scheduleWork_ scheduler $ do
        let !v = bogus p e
        Vg.unsafeWrite m e v

GHC 中的 Spark pool 也使用了工作竊取調度器,它內置於 RTS 中,與上面的 package 沒有任何形式或形式的關系,但思想非常相似:幾個線程有很多計算單元。

基准

以下是example 7中所有方法在 16 核機器上的一些基准測試(值9以秒為單位,這為criterion引入了太多噪音)。 我們只能得到大約 x5 的加速,因為算法的很大一部分本質上是順序的,不能並行化。

在此處輸入圖像描述

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM