【发布时间】:2016-01-13 22:59:20
【问题描述】:
我试图了解 Haskell(OS X 10.10.5 上的 GHC 7.10.1)中的(绿色)线程到底有多贵。我知道与真正的操作系统线程相比,它在内存使用和 CPU 方面都非常便宜。
好的,所以我开始编写一个超级简单的程序,使用 forks 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
考虑到每个线程应该只等待 10 秒,但我们已经将它构建为非线程的,所以这需要相当长的时间(1 分 33.757 秒)。总而言之,我们使用了 350 MB,这还不错,即每个线程 3.5 KB。鉴于初始堆栈大小 (-ki is 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 我看到它对于 100k 绿色线程大致使用了 4 个 OS 线程,这是有道理的。
但是,3657 MB 总内存!这比使用的非线程版本多 10 倍......
到目前为止,我没有使用 -prof 或 -hy 左右进行任何分析。为了进一步调查,我在 separate 运行中做了一些堆分析 (-hy)。在这两种情况下,内存使用都没有改变,堆分析图看起来很有趣(左:非线程,右:线程),但我找不到 10 倍差异的原因。
因此我的问题是:内存使用量的 10 倍差异来自哪里?
编辑:顺便提一下:当程序甚至没有使用分析支持进行编译时,同样的差异也适用。所以运行time ./PerTheadMem 100000 10 +RTS -sstderr 和ghc -rtsopts -threaded -fforce-recomp PerTheadMem.hs 是3559 MB。 ghc -rtsopts -fforce-recomp PerTheadMem.hs 是 395 MB。
编辑 2:在 Linux 上(GHC 7.10.2 上 Linux 3.13.0-32-generic #57-Ubuntu SMP, x86_64)也会发生同样的情况:非线程 460 MB 在 1m28.538s 和线程是 3483 MB 是 12.604s。 /usr/bin/time -v ... 分别报告Maximum resident set size (kbytes): 413684 和Maximum resident set size (kbytes): 1645384。
EDIT 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]
它不会改变任何东西:非线程:152 MB,线程:3308 MB。
【问题讨论】:
-
我想知道增加了多少开销分析。在Linux下,你可以说服
time输出内存统计信息。如果您在没有分析的情况下编译并询问操作系统的内存统计信息会发生什么? -
@MathematicalOrchid 我总共运行了四次,2 次没有分析(1 个线程/1 个非线程),2 次有分析。
-sstderr输出没有改变。图片来自后两次运行。我还检查了 Activity Monitor 中的内存使用情况,我看不出 w/ 和 w/o 分析之间的巨大差异。 -
好的,值得一试。我现在没有主意了。 :-}
-
@MathematicalOrchid 顺便说一句,我什至根本没有使用分析支持进行编译(没有
-prof -auto-all -caf-all)。 -
你能检查一下哪些闭包占用了所有内存吗?我不认为 your 代码在做任何可疑的事情,但内存占用通常是非严格计算的结果......我会尝试通过删除 mapConcurrently 来看看会发生什么,但事实并非如此使用 mapM_ 和 forkIO 生成线程需要做很多工作......
标签: multithreading haskell memory ghc