2015-10-15 21 views
19

Tôi đang cố gắng hiểu một sợi dây màu xanh lá cây đắt tiền trong Haskell (GHC 7.10.1 trên OS X 10.10.5) thực sự là như thế nào. Tôi biết rằng siêu rẻ của nó so với một chuỗi hệ điều hành thực sự, cho cả việc sử dụng bộ nhớ và cho CPU.Haskell/GHC cho mỗi chi phí bộ nhớ luồng

Phải, vì vậy tôi bắt đầu viết một chương trình siêu đơn giản với nhánh n (xanh lá cây) chủ đề (sử dụng thư viện async xuất sắc) và sau đó chỉ ngủ mỗi sợi trong m giây.

Vâng, đó là dễ dàng đủ:

$ 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] 

và trước hết, chúng ta hãy biên dịch và chạy nó:

$ 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 

rằng nên ngã ba 100k đề và chờ 10s trong mỗi và sau đó in chúng tôi một số thông tin:

$ 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 

Phải mất khá lâu (1m33,757s) cho mỗi chủ đề được cho là chỉ chỉ đợi 10 giây nhưng chúng tôi đã xây dựng nó không phải là luồng đủ công bằng cho đến bây giờ. Tất cả trong tất cả, chúng tôi sử dụng 350 MB, đó không phải là quá xấu, đó là 3,5 KB cho mỗi chủ đề. Cho rằng kích thước ngăn xếp ban đầu (-ki is 1 KB).

Đúng vậy, nhưng bây giờ chúng ta hãy biên dịch ở chế độ ren và xem liệu chúng ta có thể nhận được bất kỳ nhanh hơn:

$ 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 

Wow, nhiều nhanh hơn, chỉ cần 12s bây giờ, cách tốt hơn. Từ Activity Monitor, tôi thấy rằng nó đã sử dụng 4 chủ đề OS cho các chuỗi màu xanh lá cây 100k, điều này có ý nghĩa.

Tuy nhiên, 3657 MB tổng bộ nhớ! Hơn 10 lần so với phiên bản không phải là luồng được sử dụng ...

Cho đến bây giờ, tôi không làm bất kỳ hồ sơ nào bằng cách sử dụng -prof hoặc -hy hoặc hơn thế. Để điều tra thêm một chút, tôi đã thực hiện một số hồ sơ heap (-hy) trong riêng biệt chạy. Việc sử dụng bộ nhớ không thay đổi trong cả hai trường hợp, các biểu đồ heap hồ sơ trông thú vị khác nhau (trái: không phải luồng, phải: luồng) nhưng tôi không thể tìm thấy lý do cho sự khác biệt 10x. heap profile diffs

Khác với đầu ra hồ sơ (.prof tệp) Tôi cũng không tìm thấy bất kỳ sự khác biệt thực sự nào. prof diffs

Vì vậy, câu hỏi của tôi: Sự chênh lệch 10x trong việc sử dụng bộ nhớ đến từ đâu?

CHỈNH SỬA: Chỉ cần đề cập đến: Sự khác biệt tương tự được áp dụng khi chương trình thậm chí không được biên dịch với hỗ trợ lược tả. Vì vậy, chạy time ./PerTheadMem 100000 10 +RTS -sstderr với ghc -rtsopts -threaded -fforce-recomp PerTheadMem.hs là 3559 MB. Và với ghc -rtsopts -fforce-recomp PerTheadMem.hs là 395 MB.

EDIT 2: Trên Linux (GHC 7.10.2 trên Linux 3.13.0-32-generiC#57-Ubuntu SMP, x86_64) cùng xảy ra: Non-ren 460 MB trong 1m28.538s và ren là 3483 MB là 12.604s. /usr/bin/time -v ... báo cáo Maximum resident set size (kbytes): 413684Maximum resident set size (kbytes): 1645384 tương ứng.

EDIT 3: Cũng thay đổi chương trình để sử dụng forkIO trực tiếp:

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] 

Và nó không thay đổi bất cứ điều gì: không ren: 152 MB, luồng: 3308 MB.

+1

Tôi tự hỏi có bao nhiêu hồ sơ trên cao đang thêm. Trong Linux, bạn có thể thuyết phục 'thời gian' để thống kê bộ nhớ đầu ra. Điều gì xảy ra nếu bạn biên dịch mà không có hồ sơ và yêu cầu hệ điều hành cho số liệu thống kê bộ nhớ? – MathematicalOrchid

+1

@MathematicalOrchid Tôi đã làm bốn chạy trong tổng số, 2 mà không có hồ sơ (1 threaded/1 không-ren), 2 với hồ sơ. Đầu ra '-sstderr' không thay đổi. Những hình ảnh là từ hai sau chạy. Ngoài ra tôi đã kiểm tra sử dụng mem trong Activity Monitor và tôi không thể nhìn thấy một sự khác biệt lớn giữa w/và w/o profiling. –

+0

OK, đáng để thử. Giờ tôi không còn ý tưởng. : -} – MathematicalOrchid

Trả lời

10

IMHO, thủ phạm là threadDelay. * threadDelay ** sử dụng nhiều bộ nhớ. Đây là một chương trình tương đương với chương trình của bạn hoạt động tốt hơn với bộ nhớ. Nó đảm bảo rằng tất cả các luồng đang chạy đồng thời bằng cách tính toán lâu dài.

uBound = 38 
lBound = 34 

doSomething :: Integer -> Integer 
doSomething 0 = 1 
doSomething 1 = 1 
doSomething n | n < uBound && n > 0 = let 
        a = doSomething (n-1) 
        b = doSomething (n-2) 
       in a `seq` b `seq` (a + b) 
       | otherwise = doSomething (n `mod` uBound) 

e :: Chan Integer -> Int -> IO() 
e mvar i = 
    do 
     let y = doSomething . fromIntegral $ lBound + (fromIntegral i `mod` (uBound - lBound)) 
     y `seq` writeChan mvar y 

main = 
    do 
     args <- getArgs 
     let (numThreads, sleep) = case args of 
            numS:sleepS:[] -> (read numS :: Int, read sleepS :: Int) 
            _ -> error "wrong args" 
      dld = (sleep*1000*1000) 
     chan <- newChan 
     mapM_ (\i -> forkIO $ e chan i) [1..numThreads] 
     putStrLn "All threads created" 
     mapM_ (\_ -> readChan chan >>= putStrLn . show) [1..numThreads] 
     putStrLn "All read" 

Và đây là số liệu thống kê thời gian:

$ ghc -rtsopts -O -threaded test.hs 
$ ./test 200 10 +RTS -sstderr -N4 

133,541,985,480 bytes allocated in the heap 
    176,531,576 bytes copied during GC 
     356,384 bytes maximum residency (16 sample(s)) 
      94,256 bytes maximum slop 
       4 MB total memory in use (0 MB lost due to fragmentation) 

            Tot time (elapsed) Avg pause Max pause 
    Gen 0  64246 colls, 64246 par 1.185s 0.901s  0.0000s 0.0274s 
    Gen 1  16 colls, 15 par 0.004s 0.002s  0.0001s 0.0002s 

    Parallel GC work balance: 65.96% (serial 0%, perfect 100%) 

    TASKS: 10 (1 bound, 9 peak workers (9 total), using -N4) 

    SPARKS: 0 (0 converted, 0 overflowed, 0 dud, 0 GC'd, 0 fizzled) 

    INIT time 0.000s ( 0.003s elapsed) 
    MUT  time 63.747s (16.333s elapsed) 
    GC  time 1.189s ( 0.903s elapsed) 
    EXIT time 0.001s ( 0.000s elapsed) 
    Total time 64.938s (17.239s elapsed) 

    Alloc rate 2,094,861,384 bytes per MUT second 

    Productivity 98.2% of total user, 369.8% of total elapsed 

gc_alloc_block_sync: 98548 
whitehole_spin: 0 
gen[0].sync: 0 
gen[1].sync: 2 

cư trú tối đa là khoảng 1,5 kb cho mỗi thread. Tôi chơi một chút với số lượng các chủ đề và độ dài chạy của tính toán. Kể từ khi chủ đề bắt đầu làm công cụ ngay lập tức sau khi ngã ba, tạo ra 100000 chủ đề thực sự mất một thời gian rất dài. Nhưng kết quả được tổ chức cho 1000 chủ đề.

Dưới đây là một chương trình mà threadDelay đã được "nhân tố ra", một trong những điều này không sử dụng bất kỳ CPU và có thể được thực hiện dễ dàng với 100000 đề:

e :: MVar() -> MVar() -> IO() 
e start end = 
    do 
     takeMVar start 
     putMVar end() 

main = 
    do 
     args <- getArgs 
     let (numThreads, sleep) = case args of 
            numS:sleepS:[] -> (read numS :: Int, read sleepS :: Int) 
            _ -> error "wrong args" 
     starts <- mapM (const newEmptyMVar) [1..numThreads] 
     ends <- mapM (const newEmptyMVar) [1..numThreads] 
     mapM_ (\ (start,end) -> forkIO $ e start end) (zip starts ends) 
     mapM_ (\ start -> putMVar start()) starts 
     putStrLn "All threads created" 
     threadDelay (sleep * 1000 * 1000) 
     mapM_ (\ end -> takeMVar end) ends 
     putStrLn "All done" 

Và kết quả:

 129,270,632 bytes allocated in the heap 
    404,154,872 bytes copied during GC 
     77,844,160 bytes maximum residency (10 sample(s)) 
     10,929,688 bytes maximum slop 
      165 MB total memory in use (0 MB lost due to fragmentation) 

            Tot time (elapsed) Avg pause Max pause 
    Gen 0  128 colls, 128 par 0.178s 0.079s  0.0006s 0.0152s 
    Gen 1  10 colls,  9 par 0.367s 0.137s  0.0137s 0.0325s 

    Parallel GC work balance: 50.09% (serial 0%, perfect 100%) 

    TASKS: 10 (1 bound, 9 peak workers (9 total), using -N4) 

    SPARKS: 0 (0 converted, 0 overflowed, 0 dud, 0 GC'd, 0 fizzled) 

    INIT time 0.000s ( 0.001s elapsed) 
    MUT  time 0.189s (10.094s elapsed) 
    GC  time 0.545s ( 0.217s elapsed) 
    EXIT time 0.001s ( 0.002s elapsed) 
    Total time 0.735s (10.313s elapsed) 

    Alloc rate 685,509,460 bytes per MUT second 

    Productivity 25.9% of total user, 1.8% of total elapsed 

Trên i5 của tôi, phải mất ít hơn một giây để tạo ra 100000 chủ đề và đặt "bắt đầu" mvar. Các cư trú cao điểm là khoảng 778 byte cho mỗi chủ đề, không phải là xấu cả!


Kiểm tra thực hiện threadDelay, chúng tôi thấy rằng nó là một cách hiệu quả khác nhau đối với trường hợp ren và unthreaded:

https://hackage.haskell.org/package/base-4.8.1.0/docs/src/GHC.Conc.IO.html#threadDelay

Sau đó ở đây: https://hackage.haskell.org/package/base-4.8.1.0/docs/src/GHC.Event.TimerManager.html

trông đủ vô tội. Nhưng phiên bản cũ của cơ sở có một cách viết phức tạp của (bộ nhớ) diệt vong cho những người gọi threadDelay:

https://hackage.haskell.org/package/base-4.4.0.0/docs/src/GHC-Event-Manager.html#line-121

Nếu vẫn còn là một vấn đề hay không, thật khó để nói. Tuy nhiên, người ta luôn có thể hy vọng rằng một chương trình đồng thời "thực tế" sẽ không cần phải có quá nhiều luồng đang chờ threadDelay cùng một lúc. Tôi cho một sẽ giữ một mắt về việc sử dụng của tôi về threadDelay từ bây giờ.

+0

WOW! Tôi có thể xác nhận, chỉ cần thay đổi chương trình của tôi để sử dụng 'MVar' s quá và các con số mới là: 221 MB không luồng và 282 MB luồng. Chưa bao giờ nghĩ rằng 'threadDelay' có thể là một vấn đề. Cảm ơn rất nhiều. –

Các vấn đề liên quan