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

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

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

введите описание изображения здесь

Я довольно новичок в Хаскеле, поэтому не уверен, как мне его оптимизировать.
Есть идеи?

Ура!

1 ответ

Решение

Возможно, стоит отметить, что это оказалось простой проблемой потоковой передачи, возникающей в результате использования 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, быстро сопровождается небольшой обработкой и печатью, после чего ghc забывает все об этом и идет дальше:

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

Другие вопросы по тегам