2014-08-27 7 views
6

के लिए विशाल स्मृति खपत मेरे पास अपेक्षाकृत सरल "प्रतिलिपि" प्रोग्राम है जो केवल एक फ़ाइल की सभी पंक्तियों को प्रतिलिपि बनाता है। मैं TMQueue और STM साथ हास्केल के संगामिति समर्थन के साथ चारों ओर खेल रहा हूँ इसलिए मैंने सोचा कि मैं इसे इस तरह की कोशिश करेंगे:सरल मल्टीथ्रेडेड हास्केल

{-# 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 एमबी कुल स्मृति"! लेकिन इनपुट फाइल केवल 38 एमबी है!

मैं प्रोफाइलिंग के लिए नया हूं, लेकिन मैंने ग्राफ के बाद ग्राफ का उत्पादन किया है और मेरी गलती को इंगित नहीं कर सकता।

+0

मैं कतार को दोषी ठहराता हूं। यदि आप 'टीबीएमक्यूयू' के साथ 'टीएमक्यूयू' का आदान-प्रदान करते हैं और उचित बाध्य (कहते हैं, 10 * बैच), तो आपके पास ~ 3 एमबी कुल मेमोरी उपयोग है। – Zeta

+0

आपने '-एचसी' से क्या सीखा, और' क्यों 'दिखाता है? जब आप प्रोफाइलिंग के बिना संकलित करते हैं और केवल '+ आरटीएस-एस-एन' के साथ चलते हैं तो यह क्या कहता है? – jberryman

+0

@Zeta मैं इसे आज़माउंगा। हालांकि, मेरे वास्तविक जीवन की स्थिति में, मैं निर्माता को अवरुद्ध करने की अनुमति नहीं दे सकता। मैं बेहद उत्सुक हूं कि टीएमक्यूयू का प्रदर्शन पर इतना भयंकर प्रभाव क्यों होगा! –

उत्तर

2

ओपी के मुताबिक, अब तक मैं एक वास्तविक उत्तर भी लिख सकता हूं। चलो स्मृति खपत के साथ शुरू करते हैं।

दो उपयोगी संदर्भ Memory footprint of Haskell data types और 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 में पढ़ते हैं। उस स्थिति में, हमारे टीक्यूयू के लिखने के अंत में एक तत्व प्रति इनपुट लाइन (एक बाइटस्ट्रिंग के रूप में संग्रहीत) के साथ एक सूची होगी। प्रत्येक सूची नोड की तरह

(:) bytestring tail 

जो (निर्माता के लिए क्षेत्र +1 प्रति 1) 3 शब्द लेता दिखेगा। प्रत्येक बाइटस्ट्रिंग 9 शब्द है, इसलिए दोनों को एक साथ जोड़ें और ओवरहेड प्रति पंक्ति के वास्तविक शब्द समेत 12 शब्द हैं। आपका टेस्ट डेटा 5 मिलियन लाइन है, इसलिए पूरे फ़ाइल (साथ ही कुछ स्थिरांक) के लिए 60 मिलियन शब्द ओवरहेड है, जो कि 64-बिट सिस्टम पर लगभग 460 एमबी है (माना जाता है कि मैंने अपना गणित सही किया, हमेशा संदिग्ध)। वास्तविक डेटा के लिए 40 एमबी में जोड़ें, और हम अपने सिस्टम पर जो कुछ देखते हैं उसके करीब मूल्य प्राप्त करते हैं।

तो, हमारी स्मृति उपयोग इस ऊपरी सीमा के करीब क्यों है? मेरे पास एक सिद्धांत है (एक अभ्यास के रूप में जांच छोड़ दी गई!)। सबसे पहले, निर्माता उपभोक्ता की तुलना में थोड़ा तेज़ दौड़ने की संभावना है क्योंकि पढ़ने आमतौर पर लिखने से तेज़ होता है (मैं कताई डिस्क का उपयोग कर रहा हूं, शायद एक एसएसडी अलग होगा)। यहाँ 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 

पहले हम पढ़ छोर से पढ़ने की कोशिश, और कहा कि अगर खाली होने पर हम, लिखने अंत से पढ़ने के लिए उस सूची पीछे के बाद प्रयास करें।

मुझे लगता है कि क्या हो रहा है यह है: जब उपभोक्ता को लिखने के अंत से पढ़ने की आवश्यकता होती है, तो उसे एसटीएम लेनदेन के भीतर इनपुट सूची को पार करने की आवश्यकता होती है। इसमें कुछ समय लगता है, जिससे यह निर्माता के साथ संघर्ष कर सकता है। जैसे-जैसे निर्माता आगे बढ़ता है, यह सूची अधिक हो जाती है, जिससे पढ़ने में और अधिक समय लगता है, जिसके दौरान निर्माता अधिक मूल्य लिखने में सक्षम होता है, जिससे पढ़ने में विफल रहता है। यह प्रक्रिया उत्पादक खत्म होने तक दोहराती है, और तभी उपभोक्ता को डेटा के बड़े पैमाने पर संसाधित करने का मौका मिलता है।न केवल यह बर्बाद समरूपता है, यह अधिक सीपीयू ओवरहेड जोड़ता है क्योंकि उपभोक्ता लेनदेन निरंतर पुनः प्रयास और असफल रहा है।

तो, unagi के बारे में क्या? कुछ महत्वपूर्ण अंतर हैं। सबसे पहले, unagi-chan सूचियों के बजाय आंतरिक रूप से सरणी का उपयोग करता है। यह ओवरहेड को थोड़ा कम कर देता है। अधिकांश ओवरहेड बाइटस्ट्रिंग पॉइंटर्स से है, इसलिए ज्यादा नहीं, लेकिन थोड़ा सा। दूसरा, unagi सरणी के टुकड़े रखता है। यहां तक ​​कि अगर हम निराशाजनक रूप से मानते हैं कि निर्माता हमेशा विवाद जीतता है, तो सरणी भरने के बाद इसे चैनल के निर्माता के पक्ष से हटा दिया जाता है। अब निर्माता एक नई सरणी पर लिख रहा है और उपभोक्ता पुरानी सरणी से पढ़ता है। यह स्थिति निकट आदर्श है; साझा संसाधनों के लिए कोई विवाद नहीं है, उपभोक्ता के संदर्भ में अच्छी जगह है, और क्योंकि उपभोक्ता स्मृति के एक अलग हिस्से पर काम कर रहा है, वहां कैश समेकन के साथ कोई समस्या नहीं है। TMQueue के मेरे सैद्धांतिक वर्णन के विपरीत, अब आप समवर्ती परिचालन प्राप्त कर रहे हैं, जिससे निर्माता को स्मृति उपयोग को साफ़ करने की इजाजत मिलती है, इसलिए यह ऊपरी बाउंड को कभी भी हिट नहीं करता है।

एक तरफ, मुझे लगता है कि उपभोक्ता बैचिंग लाभकारी नहीं है। हैंडल पहले से ही आईओ उपप्रणाली द्वारा buffered हैं, तो मुझे नहीं लगता कि यह कुछ हासिल करता है। मेरे प्रदर्शन के लिए थोड़ा सुधार हुआ जब मैंने उपभोक्ता को लाइन-बाय-लाइन संचालित करने के लिए बदल दिया।

अब, आप इस समस्या के बारे में क्या कर सकते हैं? मेरी कामकाजी परिकल्पना से जाकर TMQueue विवाद की समस्याओं से पीड़ित है, और आपकी निर्दिष्ट आवश्यकताओं, आपको केवल एक और प्रकार की कतार का उपयोग करने की आवश्यकता होगी। जाहिर है unagi बहुत अच्छी तरह से काम करता है। मैंने TMChan भी कोशिश की, यह अनगी से लगभग 25% धीमी थी लेकिन 45% कम स्मृति का उपयोग किया गया, ताकि यह भी एक अच्छा विकल्प हो। (यह आश्चर्यजनक नहीं है, TMChan में TMQueue से एक अलग संरचना है, इसलिए इसमें विभिन्न प्रदर्शन विशेषताओं होंगे)

आप अपने एल्गोरिदम को बदलने का भी प्रयास कर सकते हैं ताकि निर्माता बहु-रेखा भाग भेज सके। इससे सभी बाइटस्ट्रिंग्स से मेमोरी ओवरहेड कम हो जाएगा।

तो, 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 

यह समस्या से बच जाएगा। अब z और zs बाइंडिंग दोनों का आलसी मूल्यांकन किया जाना चाहिए, इसलिए सूची लेन-देन इस लेनदेन के बाहर होगा, जिससे पढ़ने के ऑपरेशन कभी-कभी विवाद के तहत सफल हो जाते हैं। मान लीजिए कि मैं इस मुद्दे के बारे में पहले स्थान पर सही हूं (और यह परिभाषा पर्याप्त आलसी है)। हालांकि अन्य अप्रत्याशित डाउनसाइड्स हो सकते हैं।

+0

घटनात्मक उत्तर! सभी अलग कोणों से आपके संपूर्ण विश्लेषण के लिए बहुत आभारी हैं। क्या आपने अपना विकल्प 'readTQueue'' को 'stm' में संभावित वृद्धि के रूप में दर्ज करने पर विचार किया है? –

संबंधित मुद्दे