簡體   English   中英

簡單的多線程Haskell占用大量內存

[英]Huge memory consumption for simple multithreaded Haskell

我有一個相對簡單的“復制”程序,該程序僅將一個文件的所有行復制到另一個文件。 我正在使用TMQueueSTM並發支持,所以我想我可以這樣嘗試:

{-# LANGUAGE BangPatterns #-}

module Main where

import Control.Applicative
import Control.Concurrent.Async              -- from async
import Control.Concurrent.Chan
import Control.Concurrent.STM (atomically)
import Control.Concurrent.STM.TMQueue        -- from stm-chans
import Control.Monad (replicateM, forM_, forever, unless)
import qualified Data.ByteString.Char8 as B
import Data.Function (fix)
import Data.Maybe (catMaybes, maybe)
import System.IO (withFile, IOMode(..), hPutStrLn, hGetLine)
import System.IO.Error (catchIOError)

input  = "data.dat"
output = "out.dat"
batch = 100 :: Int

consumer :: TMQueue B.ByteString -> IO ()
consumer q = withFile output WriteMode $ \fh -> fix $ \loop -> do
  !items <- catMaybes <$> replicateM batch readitem
  forM_ items $ B.hPutStrLn fh
  unless (length items < batch) loop
  where
    readitem = do
      !item <- atomically $ readTMQueue q
      return item

producer :: TMQueue B.ByteString -> IO ()
producer q = withFile input ReadMode $ \fh ->
  (forever (B.hGetLine fh >>= atomically . writeTMQueue q))
  `catchIOError` const (atomically (closeTMQueue q) >> putStrLn "Done")

main :: IO ()
main = do
  q <- atomically newTMQueue
  thread <- async $ consumer q
  producer q
  wait thread

我可以像這樣制作一些測試輸入文件

ghc -e 'writeFile "data.dat" (unlines (map show [1..5000000]))'

並像這樣構建它

ghc --make QueueTest.hs -O2 -prof -auto-all -caf-all -threaded -rtsopts -o q

當我這樣運行./q +RTS -s -prof -hc -L60 -N2 ,它說“正在使用2117 MB總內存”! 但是輸入文件只有38 MB!

我是剖析的新手,但是我已經生成了一個又一個的圖,並且無法查明我的錯誤。

正如OP指出的那樣,到目前為止,我還是可以寫一個真實的答案。 讓我們從內存消耗開始。

兩個有用的參考是Haskell數據類型的內存占用量http://blog.johantibell.com/2011/06/memory-footprints-of-some-common-data.html 我們還需要查看一些結構的定義。

-- from http://hackage.haskell.org/package/stm-chans-3.0.0.2/docs/src/Control-Concurrent-STM-TMQueue.html

data TMQueue a = TMQueue
    {-# UNPACK #-} !(TVar Bool)
    {-# UNPACK #-} !(TQueue a)
    deriving Typeable


-- from http://hackage.haskell.org/package/stm-2.4.3/docs/src/Control-Concurrent-STM-TQueue.html

-- | 'TQueue' is an abstract type representing an unbounded FIFO channel.
data TQueue a = TQueue {-# UNPACK #-} !(TVar [a])
                       {-# UNPACK #-} !(TVar [a])

TQueue實現使用具有讀取端和寫入端的標准功能隊列。

讓我們設置內存使用的上限,並假設我們在使用者執行任何TMQueue之前將整個文件讀入TMQueue 在這種情況下,我們的TQueue的寫端將包含一個列表,每一輸入行包含一個元素(存儲為字節串)。 每個列表節點看起來像

(:) bytestring tail

這需要3個字(每個字段1個,構造函數1個)。 每個字節串為9個字,因此將這兩個字節加在一起, 每行的開銷為12個字,不包括實際數據。 您的測試數據為500萬行,因此整個文件的開銷為6000萬個字(加上一些常量),在64位系統上約為460MB(假設我做得對,總是有問題的)。 添加40MB的實際數據,我們得到的值非常接近我在系統上看到的值。

那么,為什么我們的內存使用量接近這個上限? 我有一個理論(作為練習剩下的調查!)。 首先,生產者的運行速度可能比消費者更快,這僅僅是因為讀取通常比寫入快(我使用旋轉磁盤,也許SSD會有所不同)。 這是readTQueue的定義:

-- |Read the next value from the 'TQueue'.
readTQueue :: TQueue a -> STM a
readTQueue (TQueue read write) = do
  xs <- readTVar read
  case xs of
    (x:xs') -> do writeTVar read xs'
                  return x
    [] -> do ys <- readTVar write
             case ys of
               [] -> retry
               _  -> case reverse ys of
                       [] -> error "readTQueue"
                       (z:zs) -> do writeTVar write []
                                    writeTVar read zs
                                    return z

首先,我們嘗試從讀取端進行讀取,如果該列為空,則在反轉該列表之后,嘗試從寫入端進行讀取。

我認為正在發生的事情是:當使用者需要從寫端讀取內容時,它需要遍歷STM事務中的輸入列表。 這需要一些時間,這將使其與生產者競爭。 隨着生產者進一步前進,此列表將更長,從而導致讀取花費更多時間,在此期間生產者能夠寫入更多值,從而導致讀取失敗。 重復此過程,直到生產者完成為止,然后消費者才有機會處理大量數據。 這不僅會破壞並發性,還會增加更多的CPU開銷,因為使用者事務不斷重試和失敗。

那么,魚呢? 有幾個主要區別。 首先,unagi-chan在內部使用數組而不是列表。 這樣可以稍微減少開銷。 大部分開銷來自ByteString指針,所以不是很多,而是很少。 其次,木會保留大量的數組。 即使我們悲觀地認為生產者總是贏得競爭,但在數組被填滿后,它會被推離通道的生產者一側。 現在,生產者正在寫一個新數組,而消費者則從舊數組中讀取。 這種情況是近乎理想的。 沒有共享資源的爭用,使用者具有良好的引用位置,並且由於使用者正在處理不同的內存塊,因此緩存一致性沒有問題。 與我對TMQueue理論描述不同,現在您可以進行並發操作,從而使生產者可以清除一些內存使用情況,從而永遠不會達到上限。

順便說一句,我認為消費者分批處理沒有好處。 句柄已經由IO子系統緩沖,因此我認為這樣做沒有任何好處。 對我來說,當我改變消費者以逐行方式進行操作時,性能會有所提高。

現在,您可以如何解決這個問題? 根據我的工作假設( TMQueue遇到爭用問題以及您的指定要求),您只需要使用另一種類型的隊列。 顯然菜效果很好。 我還嘗試了TMChan ,它比TMChan慢25%,但使用的內存卻少了45%,因此這也是一個不錯的選擇。 (這是不是太奇怪, TMChan具有不同的結構TMQueue所以它會具有不同的性能特征)

您也可以嘗試更改算法,以便生產者發送多行塊。 這將降低所有ByteString的內存開銷。

那么,什么時候可以使用TMQueue呢? 如果生產者和消費者的速度大致相同,或者消費者的速度更快,那應該沒問題。 另外,如果處理時間不一致,或者生產者突發運行,則可能會獲得良好的攤銷性能。 這幾乎是最壞的情況,也許應該將其報告為針對stm的錯誤? 我認為如果將讀取功能更改為

-- |Read the next value from the 'TQueue'.
readTQueue :: TQueue a -> STM a
readTQueue (TQueue read write) = do
  xs <- readTVar read
  case xs of
    (x:xs') -> do writeTVar read xs'
                  return x
    [] -> do ys <- readTVar write
             case ys of
               [] -> retry
               _  -> do writeTVar write []
                        let (z:zs) = reverse ys
                        writeTVar read zs
                        return z

這樣可以避免這個問題。 現在, zzs綁定都應該被懶惰地求值,因此列表遍歷將在此事務之外發生,從而使讀取操作有時在競爭下也可以成功。 當然,假設我首先對這個問題是正確的(並且這個定義足夠懶惰)。 但是,可能還有其他意外的缺點。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM