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

我在一个程序上写了几个变体。主要思想是我编写了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 --make -rtsopts -threaded -fforce-recomp -O2 DirectTableTest.hs”使用ghc 7.2.1(与7.0.3类似的结果)编译了此文件。
运行“时间./DirectTableTest 1 + RTS -N1”大约需要1.4秒,而运行“时间./DirectTableTest 2 + RTS -N2”大约需要2.0秒!使用比工作线程多的内核会更好一些,“time ./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每秒产生一次。使用4个核心运行大约需要1.2秒,这至少比1个核心好一点。更多的内核并不能改善这一点。

我发现将DirectAddressTable的实现中使用的数组类型从IOArray更改为IOUArray可以解决此问题。进行此更改后,“时间./DirectTableTest 1 + RTS -N1”的运行时间约为1.4秒,而运行的“时间./DirectTableTest 2 + RTS -N2”的运行时间约为1.0秒。增加到4个内核可提供0.55秒的运行时间。运行“-s”显示GC时间为%3.9%。在ThreadScope下,我可以看到两个线程每0.4 ms产生一次的频率比上一个程序中的产生频率更高。

最后,我尝试了另一种变化。而不是让线程在同一个共享数组上工作,而是让每个线程在自己的数组上工作。 IOArray或IOUArray可以实现DirectAddressTable数据结构,这可以很好地扩展(如您期望的那样),或多或少地类似于第二个程序。

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

最佳答案



我无法重现您的结果:

$ 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 (通过编辑)



未装箱的阵列将是连续的,因此可以从缓存中受益更多。驻留在堆中任意位置的装箱值可能会导致内核之间的缓存失效大大增加。

09-27 20:09