2013-08-15 15 views
6

Mam program Haskell, który generuje ~ 280M danych tekstowych logowania podczas biegu wewnątrz monady ST. Tutaj następuje praktycznie całkowite zużycie pamięci (przy wyłączonym protokołowaniu program przydziela łącznie 3 MB pamięci rzeczywistej).Efektywne rejestrowanie danych ciągów w Haskell's ST Monad

Problem polega na tym, że zabrakło mi pamięci. Podczas gdy program uruchamia zużycie pamięci przekracza 1,5 GB, a na końcu kończy się, gdy próbuje zapisać ciąg dziennika do pliku.

Funkcja dziennika pobiera ciąg i gromadzi dane dziennika do konstruktora ciąg przechowywane w STRef w środowisku:

import qualified Data.ByteString.Lazy.Builder as BB 
... 
myLogFunction s = do 
    ... 
    lift $ modifySTRef myStringBuilderRef (<> BB.stringUtf8 s) 

Próbowałem wprowadzenie surowości stosując wzory huk i modifySTRef”, ale to sprawiło, zużycie pamięci nawet gorzej.

piszę ciąg dziennika zgodnie z zaleceniami dokumentacji hPutBuilder, tak:

hSetBinaryMode h True 
    hSetBuffering h $ BlockBuffering Nothing 
    BB.hPutBuilder h trace 

ten zużywa kilka dodatkowych GBS pamięci. Próbowałem różnych ustawień buforowania i najpierw konwertowałem na leniwy ByteString (nieco lepiej).

Qs:

  • Jak mogę zminimalizować zużycie pamięci, gdy program działa? Spodziewałbym się, że biorąc pod uwagę ścisłą reprezentację ByteString i odpowiednią ilość ścisłości, potrzebowałbym trochę więcej pamięci niż ~ 280M rzeczywistych danych dziennika, które przechowuję.

  • Jak mogę zapisać wynik w pliku bez przydzielania pamięci? Nie rozumiem, dlaczego Haskell potrzebuje GB pamięci, aby przesłać tylko niektóre dane rezydentne do pliku.

Edit:

Oto profil pamięć o małym przebiegu (~ 42MB danych dziennika). Całkowite wykorzystanie pamięci to 3 MB z wyłączonym protokołowaniem.

15,632,058,700 bytes allocated in the heap 
    4,168,127,708 bytes copied during GC 
     343,530,916 bytes maximum residency (42 sample(s)) 
     7,149,352 bytes maximum slop 
       931 MB total memory in use (0 MB lost due to fragmentation) 

             Tot time (elapsed) Avg pause Max pause 
    Gen 0  29975 colls,  0 par 5.96s 6.15s  0.0002s 0.0104s 
    Gen 1  42 colls,  0 par 6.01s 7.16s  0.1705s 1.5604s 

    TASKS: 3 (1 bound, 2 peak workers (2 total), using -N1) 

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

    INIT time 0.00s ( 0.00s elapsed) 
    MUT  time 32.38s (33.87s elapsed) 
    GC  time 11.97s (13.31s elapsed) 
    RP  time 0.00s ( 0.00s elapsed) 
    PROF time 0.00s ( 0.00s elapsed) 
    EXIT time 0.00s ( 0.00s elapsed) 
    Total time 44.35s (47.18s elapsed) 

    Alloc rate 482,749,347 bytes per MUT second 

    Productivity 73.0% of total user, 68.6% of total elapsed 

Edit:

Pobiegłem profil pamięci o małym przebiegu dziennika jako zapytał:

profile http://imageshack.us/a/img14/9778/6a5o.png

Próbowałem dodawanie wzorców Bang !, deepseq $/$ !!, życie i takie w odpowiednich miejscach, ale nie wydaje się, żeby to miało jakikolwiek wpływ. W jaki sposób zmusić Haskella do wzięcia wyrażenia string/printf itd. I umieszczenia go w ciasnym ByteString zamiast utrzymywania wszystkich tych list [Char] i niedoszacowanych?

Edit:

Oto pełna rzeczywista funkcja śledzenia

trace s = do 
    enable <- asks envTraceEnable 
    when (enable) $ do 
     envtrace <- asks envTrace 
     let b = B8.pack s 
     lift $ b `seq` modifySTRef' envtrace (<> BB.byteString b) 

Czy to 'surowe' wystarczy? Czy muszę uważać na wszystko, jeśli zadzwonię do tej funkcji typeclass w mojej monadie ReaderT/ST? Tylko dlatego, że jest tak naprawdę wywoływana i nie odkładana w żaden sposób.

do 
    trace $ printf "%i" myint 

jest w porządku?

Dzięki!

+0

Logowanie nie dotyczy stany, a więc sugerowałbym, żebyś użył monitu Writer'a do tego – Ankur

+0

Nawet jeśli skonwertujesz z używania monady czytelnika z STRef do pisarza, mam taką samą sytuację. W końcu to monoid typu Builder. Wolałbym nie dodawać pisarza w stosie transformatora bez żadnego powodu. – NBFGRTW

+0

Potrzebujemy więcej danych. Czy możesz pokazać nam profil sterty? W jaki sposób generowany jest twój dziennik? Jeśli użyjesz na przykład 'stringUtf8', to moje podejrzenie jest takie, że wynikowy' Builder' zawiera dużą liczbę odwołań do 'String', i tam właśnie idzie pamięć. –

Odpowiedz

2

Ponieważ komunikaty rejestru zajmują tyle pamięci, wydajniejsze byłoby zapisanie ich w pliku, gdy tylko zostaną wyprodukowane. Wydaje się to niemożliwe, ponieważ znajdujemy się w monadzie ST i nie można wykonać operacji we-wy w monadzie ST.

Ale istnieje wyjście: użyj jakiegoś transformatora typu monorowego, takiego jak pakiet "rury". Oto przykład z użyciem pipes-3.3.0:

{-# LANGUAGE ExplicitForAll #-} 
{-# LANGUAGE RankNTypes #-} 
{-# LANGUAGE LiberalTypeSynonyms #-} 

import Control.Monad 
import Control.Monad.ST 
import Control.Monad.ST (stToIO) -- Transforms ST computations into IO computations 
import Control.Monad.Trans 
import Control.Monad.Morph (hoist) -- Changes the base monad of a monad transformer 
import Control.Proxy.Prelude (stdoutD) -- Consumer that prints to stdout 
import Control.Proxy.Core 
import Control.Proxy.Core.Correct 

import Data.STRef 

simpleST :: ST s Bool 
simpleST= do 
    ref <- newSTRef True 
    writeSTRef ref False 
    readSTRef ref 

-- Like simpleST, but emits log messages during the computation 
loggingST :: Producer ProxyCorrect String (ST s) Bool 
loggingST = do 
    ref <- lift $ newSTRef True 
    respond "Before writing" 
    lift $ writeSTRef ref False 
    respond "After writing" 
    lift $ readSTRef ref 

adapt :: (forall s . Producer ProxyCorrect String (ST s) a) -> 
     Producer ProxyCorrect String IO a 
adapt x = hoist stToIO x 

main :: IO() 
main = do 
    result <- runProxy $ (\_ -> adapt loggingST) >-> stdoutD 
    putStrLn . show $ result 

Drukuje dziennik na standardowe wyjście. Po uruchomieniu wyprowadza następujące:

Before writing 
After writing 
False 

To działa w następujący sposób: ty emitować komunikaty dziennika w producenta, używając respond jednocześnie przebywających w monady ST. W ten sposób możesz się zalogować i nadal mieć pewność, że twoje obliczenia nie wykonają dziwnych rzeczy IO. Zmusza cię do pieprzenia twojego kodu za pomocą dźwigów.

Po skonstruowaniu obliczenia ST, przekształca się podstawową monadę producenta ze ST na IO przy użyciu hoist. hoist to przydatna funkcja, która pozwala zmienić obrus, gdy naczynia są jeszcze na stole.

Teraz jesteśmy w IO-land! Pozostaje tylko połączyć producenta z konsumentem, który faktycznie zapisuje wiadomości (tutaj są drukowane na standardowe wyjście, ale równie łatwo można połączyć się z konsumentem, który zapisuje do pliku.)

+1

Muszę przyznać, że to trochę ponad moją głowę, ale zawsze chciałem sprawdzić out the pipe package! Ale dla jasności, nie ma absolutnie żadnego problemu z rozmiarem komunikatów logów 280MB jest całkowicie w porządku! Problemem jest Haskell marnujący GB na "rzeczy Haskella" .Jeśli zaimplementowałem moje rejestrowanie używając zmiennego nieskrytego wektora z 500MB Word8s i po prostu napisałem to do pliku, wszystko byłoby w porządku.Po prostu pytam, czy ktoś może mi powiedzieć, dlaczego biblioteka ByteString potrzebuje 5x więcej pamięci niż oczekiwano, a następnie przydziela GBs pamięci tymczasowej tylko po to, aby zapisać te dane do pliku. – NBFGRTW