Я пытаюсь понять, насколько дорогой (зеленый) поток в Haskell (GHC 7.10.1 на OS X 10.10.5) действительно. Я знаю, что это супер дешево по сравнению с реальным потоком ОС, как для использования памяти, так и для процессора.
Итак, я начал писать суперпростую программу с вилками n
(зеленых) потоков (используя отличную библиотеку async
) а затем просто спит каждый поток за m
секунды.
Ну, это достаточно легко:
$ cat PerTheadMem.hs
import Control.Concurrent (threadDelay)
import Control.Concurrent.Async (mapConcurrently)
import System.Environment (getArgs)
main = do
args <- getArgs
let (numThreads, sleep) = case args of
numS:sleepS:[] -> (read numS :: Int, read sleepS :: Int)
_ -> error "wrong args"
mapConcurrently (\_ -> threadDelay (sleep*1000*1000)) [1..numThreads]
и прежде всего, скомпилируйте и запустите его:
$ ghc --version
The Glorious Glasgow Haskell Compilation System, version 7.10.1
$ ghc -rtsopts -O3 -prof -auto-all -caf-all PerTheadMem.hs
$ time ./PerTheadMem 100000 10 +RTS -sstderr
который должен использовать потоки 100k и ждать 10 секунд в каждом, а затем распечатать нам некоторую информацию:
$ time ./PerTheadMem 100000 10 +RTS -sstderr
340,942,368 bytes allocated in the heap
880,767,000 bytes copied during GC
164,702,328 bytes maximum residency (11 sample(s))
21,736,080 bytes maximum slop
350 MB total memory in use (0 MB lost due to fragmentation)
Tot time (elapsed) Avg pause Max pause
Gen 0 648 colls, 0 par 0.373s 0.415s 0.0006s 0.0223s
Gen 1 11 colls, 0 par 0.298s 0.431s 0.0392s 0.1535s
INIT time 0.000s ( 0.000s elapsed)
MUT time 79.062s ( 92.803s elapsed)
GC time 0.670s ( 0.846s elapsed)
RP time 0.000s ( 0.000s elapsed)
PROF time 0.000s ( 0.000s elapsed)
EXIT time 0.065s ( 0.091s elapsed)
Total time 79.798s ( 93.740s elapsed)
%GC time 0.8% (0.9% elapsed)
Alloc rate 4,312,344 bytes per MUT second
Productivity 99.2% of total user, 84.4% of total elapsed
real 1m33.757s
user 1m19.799s
sys 0m2.260s
Потребовалось довольно много времени (1m33.757s), учитывая, что каждый поток должен только ждать 10 секунд, но сейчас мы построили его без потоковой передачи. В общем, мы использовали 350 МБ, что не так уж плохо, что 3,5 КБ на поток. Учитывая, что размер начального стека (-ki
равен 1 KB).
Правильно, но теперь пусть компиляция находится в поточном режиме и посмотрим, получим ли мы быстрее:
$ ghc -rtsopts -O3 -prof -auto-all -caf-all -threaded PerTheadMem.hs
$ time ./PerTheadMem 100000 10 +RTS -sstderr
3,996,165,664 bytes allocated in the heap
2,294,502,968 bytes copied during GC
3,443,038,400 bytes maximum residency (20 sample(s))
14,842,600 bytes maximum slop
3657 MB total memory in use (0 MB lost due to fragmentation)
Tot time (elapsed) Avg pause Max pause
Gen 0 6435 colls, 0 par 0.860s 1.022s 0.0002s 0.0028s
Gen 1 20 colls, 0 par 2.206s 2.740s 0.1370s 0.3874s
TASKS: 4 (1 bound, 3 peak workers (3 total), using -N1)
SPARKS: 0 (0 converted, 0 overflowed, 0 dud, 0 GC'd, 0 fizzled)
INIT time 0.000s ( 0.001s elapsed)
MUT time 0.879s ( 8.534s elapsed)
GC time 3.066s ( 3.762s elapsed)
RP time 0.000s ( 0.000s elapsed)
PROF time 0.000s ( 0.000s elapsed)
EXIT time 0.074s ( 0.247s elapsed)
Total time 4.021s ( 12.545s elapsed)
Alloc rate 4,544,893,364 bytes per MUT second
Productivity 23.7% of total user, 7.6% of total elapsed
gc_alloc_block_sync: 0
whitehole_spin: 0
gen[0].sync: 0
gen[1].sync: 0
real 0m12.565s
user 0m4.021s
sys 0m1.154s
Ничего себе, гораздо быстрее, всего 12 секунд, лучше. Из Activity Monitor я увидел, что он примерно использовал 4 потока ОС для зеленых потоков 100 тыс., Что имеет смысл.
Однако 3657 МБ общей памяти! Это в 10 раз больше, чем не-резьбовая версия...
До сих пор я не делал профилирования с использованием -prof
или -hy
или так. Чтобы исследовать немного больше, я сделал некоторое профилирование кучи (-hy
) в отдельных прогонах. Использование памяти в обоих случаях не изменилось, графики профилирования кучи выглядят интересным образом (слева: не-threaded, right: threaded), но я не могу найти причину разницы в 10 раз.
Разграничение вывода профилирования (.prof
файлов) Я также не могу найти никакой реальной разницы.
Поэтому мой вопрос: откуда происходит 10-кратное различие в использовании памяти?
РЕДАКТИРОВАТЬ: просто упомянуть об этом: та же разница применяется, когда программа даже не скомпилирована с поддержкой профилирования. Таким образом, запуск time ./PerTheadMem 100000 10 +RTS -sstderr
с ghc -rtsopts -threaded -fforce-recomp PerTheadMem.hs
равен 3559 МБ. И с ghc -rtsopts -fforce-recomp PerTheadMem.hs
он 395 МБ.
РЕДАКТИРОВАТЬ 2. В Linux (GHC 7.10.2
on Linux 3.13.0-32-generic #57-Ubuntu SMP, x86_64
) происходит одно и то же: Non-threaded 460 МБ в 1m28.538s и threaded составляет 3483 МБ - 12,604s. /usr/bin/time -v ...
сообщает Maximum resident set size (kbytes): 413684
и Maximum resident set size (kbytes): 1645384
соответственно.
РЕДАКТИРОВАТЬ 3. Также была изменена программа для непосредственного использования forkIO
:
import Control.Concurrent (threadDelay, forkIO)
import Control.Concurrent.MVar
import Control.Monad (mapM_)
import System.Environment (getArgs)
main = do
args <- getArgs
let (numThreads, sleep) = case args of
numS:sleepS:[] -> (read numS :: Int, read sleepS :: Int)
_ -> error "wrong args"
mvar <- newEmptyMVar
mapM_ (\_ -> forkIO $ threadDelay (sleep*1000*1000) >> putMVar mvar ())
[1..numThreads]
mapM_ (\_ -> takeMVar mvar) [1..numThreads]
И это ничего не меняет: non-threaded: 152 MB, threaded: 3308 MB.