2017-01-28 44 views
3

我在Haskell一個下面的程序:減少一個Haskell程序的內存佔用

processDate :: String -> IO() 
processDate date = do 
    ... 
    let newFlattenedPropertiesWithPrice = filter (notYetInserted date existingProperties) flattenedPropertiesWithPrice 
    geocodedProperties <- propertiesWithGeocoding newFlattenedPropertiesWithPrice 

propertiesWithGeocoding :: [ParsedProperty] -> IO [(ParsedProperty, Maybe LatLng)] 
propertiesWithGeocoding properties = do 
    let addresses = fmap location properties 
    let batchAddresses = chunksOf 100 addresses 
    batchGeocodedLocations <- mapM geocodeAddresses batchAddresses 
    let geocodedLocations = fromJust $ concat <$> sequence batchGeocodedLocations 
    return (zip properties geocodedLocations) 

geocodeAddresses :: [String] -> IO (Maybe [Maybe LatLng]) 
geocodeAddresses addresses = do 
    mapQuestKey <- getEnv "MAP_QUEST_KEY" 
    geocodeResponse <- openURL $ mapQuestUrl mapQuestKey addresses 
    return $ geocodeResponseToResults geocodeResponse 

geocodeResponseToResults :: String -> Maybe [Maybe LatLng] 
geocodeResponseToResults inputResponse = 
    latLangs 
    where 
     decodedResponse :: Maybe GeocodingResponse 
     decodedResponse = decodeGeocodingResponse inputResponse 

     latLangs = fmap (fmap geocodingResultToLatLng . results) decodedResponse 

decodeGeocodingResponse :: String -> Maybe GeocodingResponse 
decodeGeocodingResponse inputResponse = Data.Aeson.decode (fromString inputResponse) :: Maybe GeocodingResponse 

它讀取從HTML文件屬性(住宅和公寓)的列表,分析它們,地理編碼地址並保存結果到sqlite數據庫。
除了非常高的內存使用率(大約800M)之外,一切正常。
通過對代碼進行評論,我指出問題是地理編碼步驟。
我一次發送100個地址到MapQuest API(https://developer.mapquest.com/documentation/geocoding-api/batch/get/)。
100個地址的響應非常大,所以它可能是其中一個匪徒,但800M?我感覺所有的結果都持續到驅動內存使用這麼高的結尾。

在註釋掉程序的地理編碼部分之後,內存使用量大約在30M左右,這很好。

你可以得到完整的版本,這裏再現了問題:https://github.com/Leonti/haskell-memory-so

enter image description here

我在Haskell相當新手,所以不知道如何優化它。
任何想法?

乾杯!

+0

我懷疑GC沒有啓動,因爲你有足夠的內存可用,而不運行GC要比不必要地運行更快。在GC-ed語言中這是一種非常常見的模式。嘗試限制堆可用,看看它是否仍然適合。 – 9000

+1

@ 9000這不太可能有幫助。關於中間結果持續太久,OP可能是正確的。 'propertiesWithGeocoding'中的'mapM'是一個可能的罪魁禍首(如果確實如此,這裏的答案可能涉及到流式庫,例如* pipes *和* conduit *,它們通常用於提供'mapM'的替代方案當處理大量數據時)。 – duplode

+1

我希望我有一個可運行的片段。解決方案可能部分是使用流媒體庫,如「streaming」或「conduit」或「pipes」。但還有其他的特點。首先記住,aeson傾向於積累內存中用於解析的所有輸入。 (這是唯一可以適用於所有json的方法)。有一個'json-stream'庫可以在某些情況下解決這個問題,這取決於你在json中尋找什麼。 – Michael

回答

1

這可能是值得記錄的是這竟然是因使用mapMsequence,這與replicateMtraverse和其他東西,讓你「從IO提取列表」引起的simple streaming problem隨時增加積累的擔憂。所以需要一個流媒體庫繞道。因此,在回購有必要只是像這樣的東西

import Streaming 
import qualified Streaming.Prelude as S 

processDate :: String -> IO() 
processDate date = do 
    allFiles <- listFiles date -- we accept an unstreamed list 
    S.print $ propertiesWithGeocoding -- this was the main pain point see below 
      $ S.filter hasPrice 
      $ S.concat 
      $ S.mapM fileToProperties -- this mapM doesn't accumulate 
      $ S.each allFiles -- the list is converted to a stream 

propertiesWithGeocoding 
    :: Stream (Of ParsedProperty) IO r 
    -> Stream (Of (ParsedProperty, Maybe LatLng)) IO r 
propertiesWithGeocoding properties = 
    S.concat $ S.concat 
      $ S.mapM geocodeAddresses -- this mapM doesn't accumulate results from mapquest 
      $ S.mapped S.toList  -- convert segments to haskell lists 
      $ chunksOf 100 properties -- this is the streaming `chunksOf` 
    -- concat here flattens a stream of lists of as into a stream of as 
    -- and a stream of maybe as into a stream of as 

更換

processDate :: String -> IO() 
processDate date = do 
    allFiles <- listFiles date 
    allProperties <- mapM fileToProperties allFiles 
    let flattenedPropertiesWithPrice = filter hasPrice $ concat allProperties 
    geocodedProperties <- propertiesWithGeocoding flattenedPropertiesWithPrice 
    print geocodedProperties 

propertiesWithGeocoding :: [ParsedProperty] -> IO [(ParsedProperty, Maybe LatLng)] 
propertiesWithGeocoding properties = do 
    let batchProperties = chunksOf 100 properties 
    batchGeocodedLocations <- mapM geocodeAddresses batchProperties 
    let geocodedLocations = fromJust $ concat <$> sequence batchGeocodedLocations 
    return geocodedLocations 

那麼內存的使用看起來像這樣,相當於一趟Mapquest服務promply隨後稍作加工各峯和打印,於是ghc忘記所有關於它和移動上:

當然這一點可以用pipesconduit完成。但在這裏,我們只需要一點點簡單的mapM/sequence/traverse/replicateM迴避和streaming對於這種快速本地重構可能是最簡單的。請注意,這個列表非常短,所以想到'但是簡短的列表很酷很有用mapM/traverse/etc!「可能是相當驚人的錯誤,爲什麼不擺脫它們?每當你要寫清單mapM f這是一個很好的想法考慮S.mapM f . S.each(或相當於管道或管道)您現在將擁有一個流,並可以通過S.toList或等價物恢復列表,但很可能您會發現您不需要通知累積列表,但可以例如使用一些流式處理如打印到文件或標準輸出或將事物寫入數據庫,在做出任何需要操縱的列表之後(在這裏我們使用例如流式傳輸filter以及concat來扁平化流式列表並且作爲catMaybe)。

相關問題