在Haskell中同时读写IOArray

时间:2011-08-25 17:43:20

标签: haskell concurrency ghc

我正在努力在Haskell中使用GHC为多核机器编写并发程序。作为第一步,我决定编写一个同时读写IOArray的程序。我的印象是对IOArray的读写操作不涉及同步。我这样做是为了建立一个基线来与使用适当同步机制的其他数据结构的性能进行比较。我遇到了一些令人惊讶的结果,即在很多情况下,我根本没有加速。这让我想知道在ghc运行时是否发生了一些低级同步,例如同步和阻塞评估thunks(即“黑洞”)。以下是详细信息......

我在一个程序上写了几个变体。主要的想法是我编写了一个DirectAddressTable数据结构,它只是一个提供插入和查找方法的IOArray的包装器:

-- file DirectAddressTable.hs
module DirectAddressTable 
       ( DAT
       , newDAT
       , lookupDAT
       , insertDAT
       , getAssocsDAT
       )
       where

import Data.Array.IO
import Data.Array.MArray

newtype DAT = DAT (IOArray Int Char)

-- create a fixed size array; missing keys have value '-'.
newDAT :: Int -> IO DAT
newDAT n = do a <- newArray (0, n - 1) '-'
              return (DAT a)

-- lookup an item.
lookupDAT :: DAT -> Int -> IO (Maybe Char)
lookupDAT (DAT a) i = do c <- readArray a i 
                         return (if c=='-' then Nothing else Just c)

-- insert an item
insertDAT :: DAT -> Int -> Char -> IO ()
insertDAT (DAT a) i v = writeArray a i v

-- get all associations (exclude missing items, i.e. those whose value is '-').
getAssocsDAT :: DAT -> IO [(Int,Char)]
getAssocsDAT (DAT a) = 
  do assocs <- getAssocs a
     return [ (k,c) | (k,c) <- assocs, c /= '-' ]

然后我有一个初始化一个新表的主程序,分叉一些线程,每个线程写入并读取一些固定数量的值到刚刚初始化的表。要写入的元素总数是固定的。要使用的线程数取自命令行参数,要处理的元素在线程之间平均分配。

-- file DirectTableTest.hs
import DirectAddressTable
import Control.Concurrent
import Control.Parallel
import System.Environment

main = 
  do args <- getArgs
     let numThreads = read (args !! 0)
     vs <- sequence (replicate numThreads newEmptyMVar)
     a <- newDAT arraySize     
     sequence_ [ forkIO (doLotsOfStuff numThreads i a >>= putMVar v) 
               | (i,v) <- zip [1..] vs]
     sequence_ [ takeMVar v >>= \a -> getAssocsDAT a >>= \xs -> print (last xs)  
               | v <- vs]     

doLotsOfStuff :: Int -> Int -> DAT -> IO DAT
doLotsOfStuff numThreads i a = 
  do let p j c = (c `seq` insertDAT a j c) >> 
                 lookupDAT a j >>= \v -> 
                 v `pseq` return ()  
     sequence_ [ p j c | (j,c) <- bunchOfKeys i ]
     return a
  where  bunchOfKeys i = take numElems $ zip cyclicIndices $ drop i cyclicChars
         numElems      = numberOfElems `div` numThreads

cyclicIndices = cycle [0..highestIndex]
cyclicChars   = cycle chars
chars         = ['a'..'z']

-- Parameters
arraySize :: Int
arraySize     = 100
highestIndex  = arraySize - 1
numberOfElems = 10 * 1000 * 1000     

我使用ghc 7.2.1(与7.0.3类似的结果)使用“ghc --make -rtsopts -threaded -fforce-recomp -O2 DirectTableTest.hs”编译了这个。 运行“time ./DirectTableTest 1 + RTS -N1”大约需要1.4秒,运行“time ./DirectTableTest 2 + RTS -N2”需要大约2.0秒!使用一个核心而不是工作线程更好一点,“时间./DirectTableTest 1 + RTS -N1”需要大约1.4秒并运行“time ./DirectTableTest 1 + RTS -N2”和“time ./DirectTableTest 2 + RTS” -N3“两者都需要大约1.4秒。 使用“-N2 -s”选项运行表明生产率为95.4%,GC为4.3%。用ThreadScope查看程序的运行情况,我没有看到任何太令人担忧的事情。当GC发生时,每个HEC每ms产生一次。使用4个核心运行时间约为1.2秒,至少比1核心好一点。更多核心并没有改善这一点。

我发现将从IOArray实现DirectAddressTable中使用的数组类型更改为IOUArray可以解决此问题。通过这种改变,“time ./DirectTableTest 1 + RTS -N1”的运行时间约为1.4秒,而运行“time ./DirectTableTest 2 + RTS -N2”的运行时间约为1.0秒。增加到4个核心的运行时间为0.55秒。使用“-s”运行表示GC时间为%3.9%。在ThreadScope下,我可以看到两个线程每0.4毫秒产生一次,比以前的程序更频繁。

最后,我尝试了另外一种变体。我没有让线程在同一个共享数组上工作,而是让每个线程都在自己的数组上工作。这可以很好地扩展(如你所料),或多或少像第二个程序,IOArray或IOUArray实现DirectAddressTable数据结构。

我理解为什么IOUArray可能比IOArray表现得更好,但我不知道为什么它可以更好地扩展到多个线程和核心。有谁知道为什么会发生这种情况或者我能做些什么来找出发生了什么?我想知道这个问题是否可能是由于多个线程在评估同一个thunk时阻塞,以及它是否与此相关:http://hackage.haskell.org/trac/ghc/ticket/3838

1 个答案:

答案 0 :(得分:2)

  

运行“time ./DirectTableTest 1 + RTS -N1”大约需要1.4秒,运行“time ./DirectTableTest 2 + RTS -N2”需要大约2.0秒!

我无法重现您的结果:

$ time ./so2 1 +RTS -N1
(99,'k')

real    0m0.950s
user    0m0.932s
sys     0m0.016s
tommd@Mavlo:Test$ time ./so2 2 +RTS -N2
(99,'s')
(99,'s')

real    0m0.589s
user    0m1.136s
sys     0m0.024s

这似乎随着轻量级线程数量的增加而按预期扩展:

ghc -O2 so2.hs -threaded -rtsopts
[1 of 2] Compiling DirectAddressTable2 ( DirectAddressTable2.hs, DirectAddressTable2.o )
[2 of 2] Compiling Main             ( so2.hs, so2.o )
Linking so2 ...
tommd@Mavlo:Test$ time ./so2 4
(99,'n')
(99,'z')
(99,'y')
(99,'y')

real    0m1.538s
user    0m1.320s
sys     0m0.216s
tommd@Mavlo:Test$ time ./so2 4 +RTS -N2
(99,'z')
(99,'x')
(99,'y')
(99,'y')

real    0m0.600s
user    0m1.156s
sys     0m0.020s

你真的有2个CPU吗?如果你使用更多的GHC线程(-Nx)而不是你拥有的CPU,那么你的结果将非常糟糕。我认为我真正想问的是:您确定系统上没有其他CPU密集型进程在运行吗?

关于IOUArray (通过编辑)

  

我理解为什么IOUArray可能比IOArray表现更好,但我不知道为什么它可以更好地扩展到多个线程和核心

未装箱的阵列将是连续的,因此从缓存中获益更多。生活在堆上任意位置的盒装值可能会导致核心之间的缓存失效大幅增加。