Объем памяти в многопоточном режиме с forkIO при большом количестве конфликтов для одного MVar - PullRequest
2 голосов
/ 15 января 2012

Приведенный ниже код является всего лишь экспериментом, чтобы увидеть, что происходит, когда вызывается forkIO (чтобы проверить издержки облегченного потока GHC - и накладные расходы памяти, и эффект конкуренции для MVar) с функцией, которая просто читает и записывает MVar. Я знаю, что будет конфликт доступа, когда 1 миллион потоков ожидает доступа к MVar - но поскольку каждая функция обращается к нему только один раз, все они должны в конечном итоге пройти. В моих тестах я вижу, что если я компилирую код с опцией -threaded (GHC 7.0.3, Mac, x86_64), код сильно замедляется - в ~ 110 раз. Я буду признателен за указатели на причину замедления в коде с опцией -threaded.

{-# LANGUAGE BangPatterns #-}
import Control.Concurrent (forkIO, MVar, newEmptyMVar, putMVar, takeMVar)
import qualified Data.Vector.Storable.Mutable as MSV
import Control.Monad.Primitive (PrimState)
import Control.Monad (mapM_)

f ::  MVar Int -> IO ()
f m  = do
            !i <- takeMVar m
            putMVar m (i+1)

main = do
  m <- newEmptyMVar
  putMVar m 0
  let l = 1000000
  mapM_ (\x -> forkIO $ f m) [1..l]

При компиляции с ghc -O2 -rtsopts:

 ./test +RTS -s 
   1,070,652,216 bytes allocated in the heap
   1,023,908,744 bytes copied during GC
           1,872 bytes maximum residency (1 sample(s))
         177,328 bytes maximum slop
               9 MB total memory in use (0 MB lost due to fragmentation)

  Generation 0:  2029 collections,     0 parallel,  0.13s,  0.13s elapsed
  Generation 1:     1 collections,     0 parallel,  0.00s,  0.00s elapsed

  INIT  time    0.00s  (  0.00s elapsed)
  MUT   time    0.08s  (  0.08s elapsed)
  GC    time    0.13s  (  0.13s elapsed)
  EXIT  time    0.00s  (  0.00s elapsed)
  Total time    0.21s  (  0.22s elapsed)

  %GC time      61.9%  (61.7% elapsed)

  Alloc rate    13,110,134,156 bytes per MUT second

  Productivity  37.9% of total user, 37.2% of total elapsed

При компиляции с ghc -O2 -rtsopts -threaded (я использовал -N3 для четырехъядерного IMAC):

./test +RTS -s -N3 
   1,096,608,080 bytes allocated in the heap
   2,713,129,232 bytes copied during GC
     761,160,288 bytes maximum residency (10 sample(s))
     711,798,176 bytes maximum slop
            2424 MB total memory in use (0 MB lost due to fragmentation)

  Generation 0:  1177 collections,  1176 parallel, 46.51s, 15.93s elapsed
  Generation 1:    10 collections,    10 parallel,  9.35s,  5.60s elapsed

  Parallel GC work balance: 1.05 (339027672 / 323162843, ideal 3)

                        MUT time (elapsed)       GC time  (elapsed)
  Task  0 (worker) :    0.00s    (  0.00s)       0.00s    (  0.00s)
  Task  1 (worker) :   56.95s    (  1.49s)       0.26s    (  0.09s)
  Task  2 (worker) :   57.05s    (  1.49s)       0.16s    (  0.05s)
  Task  3 (bound)  :    1.49s    (  1.23s)      55.27s    ( 21.33s)
  Task  4 (worker) :   57.20s    (  1.49s)       0.00s    (  0.00s)
  Task  5 (worker) :   57.03s    (  1.49s)       0.18s    (  0.06s)

  SPARKS: 0 (0 converted, 0 pruned)

  INIT  time    0.00s  (  0.00s elapsed)
  MUT   time    0.90s  (  1.23s elapsed)
  GC    time   55.86s  ( 21.53s elapsed)
  EXIT  time    0.45s  (  0.40s elapsed)
  Total time   57.21s  ( 23.02s elapsed)

  %GC time      97.6%  (93.5% elapsed)

  Alloc rate    811,808,581 bytes per MUT second

  Productivity   2.4% of total user, 5.9% of total elapsed

gc_alloc_block_sync: 19789
whitehole_spin: 73
gen[0].sync_large_objects: 0
gen[1].sync_large_objects: 0

Как вы можете видеть выше, общий объем используемой памяти увеличивается с ~ 9 МБ до ~ 2 ГБ в режиме -threaded. Общий объем памяти, выделяемой в куче, в обоих случаях находится в пределах ~ 1%. Я подозреваю, что большая часть резидентной памяти возникает из-за разветвления функции, поскольку каждый экземпляр функции в куче должен быть thunk. Так как я не использовал ни одну из стратегий номинала, искр нет.

Я написал этот код, потому что мне интересно поведение с режимом -threaded и без него, а не потому, что я планирую использовать его таким образом. По сути, я учу себя, что произойдет, если ты напишешь такой плохой код. Таким образом, вы не должны говорить, что не пишите так:)

РЕДАКТИРОВАТЬ 1: ehird указал в комментарии, чтобы проверить, что threaded сама среда выполнения не способствует замедлению. Похоже, что многопотоковое выполнение не способствует замедлению - компиляция с опцией -threaded не влияет на производительность. Замедление происходит только при использовании опции +RTS -N во время выполнения.

1 Ответ

4 голосов
/ 15 января 2012

Тестовая программа НЕ выполняет ту работу, которую вы ожидаете. Как только поток main завершается, он завершается, и вся программа завершается.

Без многопоточной среды выполнения это происходит довольно быстро. С потоками среда выполнения может переключаться и выполнять некоторые работы, но не все работы до завершения потока main.

Вам нужно изменить дизайн теста, если вы хотите, чтобы этот микробенчмарк работал:

{-# LANGUAGE BangPatterns #-}
import Control.Concurrent (forkIO, MVar, newEmptyMVar, putMVar, takeMVar, threadDelay)
import Control.Monad (mapM_)

l = 100000

f ::  MVar Int -> IO ()
f m  = do
            !i <- takeMVar m
            putMVar m (i+1)
            if i==l then print "done" else return ()


main = do
  m <- newEmptyMVar
  putMVar m 1
  mapM_ (\x -> forkIO $ f m) [1..l]
--  threadDelay (1000*1000)

Я немного изменил значение l и начал m со значения 1. Он будет печатать «готово» только тогда, когда основной поток задерживается достаточно долго, чтобы позволить последнему шагу сделать последний шаг i.

Если вы измените print "done" на команду выхода из программы, тогда вы можете рассчитать время выполнения.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...