2017-01-28 15 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 db.
Все работает отлично, за исключением очень большого использования памяти (около 800 м).
Прокомментировав код, я определил проблему как шаг геокодирования.
Я отправляю 100 адресов одновременно MapQuest api (https://developer.mapquest.com/documentation/geocoding-api/batch/get/).
Ответ на 100 адресов довольно массивный, поэтому он может быть одним из виновников, но 800M? Я чувствую, что он держится за все результаты до конца, которые так сильно влияют на использование памяти.

После комментирования часть геокодирования для использования памяти программы составляет около 30 М, что прекрасно.

Вы можете получить полную версию, которая воспроизводит этот вопрос здесь: https://github.com/Leonti/haskell-memory-so

enter image description here

Я совсем новичок в Haskell, так что не знаю, как я могу оптимизировать его.
Любые идеи?

Cheers!

+0

Я подозреваю, что GC просто не влезает, потому что у вас достаточно доступной ОЗУ и не работает GC быстрее, чем бегать без необходимости. Это довольно распространенный образец на языках GC-ed. Попробуйте ограничить доступность кучи и посмотрите, подходит ли она. – 9000

+1

@ 9000 Это вряд ли поможет. Вероятно, ПП справедлив в том, что промежуточные результаты проводятся слишком долго. «MapM» в 'propertiesWithGeocoding' является вероятным виновником (и, если это так, ответы здесь скорее всего будут включать потоковые библиотеки, такие как * pipe * и * conduit *, которые обычно используются для предоставления альтернатив' mapM' при работе с большими объемами данных). – duplode

+1

Хотелось бы, чтобы у меня был бегущий фрагмент. Решение может частично состоять в использовании потоковой библиотеки, такой как «потоковая передача» или «канал» или «труба». Но есть и другие особенности. Имейте в виду, прежде всего, что аэзон имеет тенденцию накапливать весь ввод в памяти для синтаксического анализа. (Это единственный способ сделать это для всех json). Существует библиотека 'json-stream', которая может обойти это в некоторых случаях в зависимости от того, что вы ищете в json. – Michael

ответ

1

Может быть, стоит запись, что это оказалось бы simple streaming problem, вытекающие из использования mapM и sequence, который с replicateM и traverse и других вещей, которые делают вас «извлечь список из IO» всегда поднимают накопление тревоги. Поэтому понадобился небольшой объезд из потоковой библиотеки. Таким образом, в репо, что было необходимо просто заменить

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 

что-то вроде этого

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 

Тогда использование памяти выглядит так, каждый пик, соответствующая поездка в Mapquest promply с последующей небольшой обработкой и печать, после чего ghc забывает о ней и движется на:

конечно, это может быть сделано с помощью pipes или conduit. Но здесь нам просто нужно немного простота mapM/sequence/traverse/replicateM избежание и streaming, возможно, проще всего для такого быстрого локального рефакторинга. Обратите внимание, что этот список довольно короткий, поэтому мысли «но короткие списки классные с mapM/traverse/и т. Д.» Могут быть довольно эффектно ложными. Почему бы просто не избавиться от них? Всякий раз, когда вы собираетесь написать список mapM f, это хорошо Идея рассмотреть S.mapM f . S.each (или эквивалент трубопровода или труб). Теперь у вас будет поток и вы можете восстановить список с S.toList или эквивалентом, но вполне вероятно, что, как и в этом случае, вы обнаружите, что вам не нужна оверенность накопленный список, но может, например, использовать некоторый процесс потоковой передачи, такой как печать в файл или stdout или запись вещей в базу данных, после того, как нужен какой-либо список, например, манипуляции (здесь мы используем, например, потоковое filter, а также concat, чтобы сгладить потоковые списки и как своего рода catMaybe).