Анализ производительности сворачивания с использованием ключей map и ByteString

У меня есть небольшой скрипт для чтения, анализа и получения какой-то интересной (не совсем) статистики из файла журнала apache. Пока что я сделал два простых варианта: общее количество байтов, отправленных во всех запросах в файле журнала, и 10 самых распространенных IP-адресов.

Первый «режим» - это просто сумма всех проанализированных байтов. Второй - это загиб карты (Data.Map) с использованием insertWith (+) 1' для подсчета вхождений.

Первый запускается, как я и ожидал, большую часть времени, потраченного на синтаксический анализ, в постоянном пространстве.

42 359 709 344 байта, выделенные в куче 72 405 840 байтов, скопированных во время GC 113 712 байтов, максимальное время размещения (1553 выборки) 145 872 байта, максимальное время ожидания 2 МБ общей используемой памяти (0 МБ потеряно из-за фрагментации)

Поколение 0: 76311 собраний,
0 параллелей, 0,89 с, прошло 0,99 с.
Поколение 1: 1553 собраний, 0 параллелей, 0,21 с, прошло 0,22 с.

Время INIT 0,00 с (прошло 0,00 с) Время MUT 21,76 с (прошло 24,82 с) Время GC 1,10 с (прошло 1,20 с) Время выхода
0,00 с (прошло 0,00 с) Общее время 22,87 с (прошло 26,02 с)

% Времени GC 4,8% (прошло 4,6%)

Скорость выделения 1 946 258 962 байта в секунду MUT

Производительность 95,2% от общего числа пользователей, 83,6% от общего количества затраченных средств

Однако второй нет!

49 398 834 152 байта, выделенные в куче 580 579 208 байтов, скопированных во время GC 718 385 088 байтов максимальное время размещения (15 выборок) 134 532 128 байтов максимальное временное пространство 1393 МБ общей используемой памяти (172 МБ потеряно из-за фрагментации)

Поколение 0: 91275 коллекций,
0 параллелей, 252,65 с, прошло 254,46 с
Поколение 1:15 коллекций, 0 параллелей, 0,12 с, 0,12 с истекло

Время INIT 0,00 с (прошло 0,00 с) Время MUT 41,11 с (прошло 48,87 с) Время GC 252,77 с (прошло 254,58 с) Время выхода
0,00 с (прошло 0,01 с) Общее время 293,88 с (прошло 303,45 с)

% Времени GC 86,0% (прошло 83,9%)

Скорость выделения 1201635385 байт в секунду MUT

Производительность 14,0% от общего числа пользователей, 13,5% от общего числа затраченных

А вот и код.

{-# LANGUAGE OverloadedStrings #-}

module Main where

import qualified Data.Attoparsec.Lazy as AL
import Data.Attoparsec.Char8 hiding (space, take)
import qualified Data.ByteString.Char8 as S
import qualified Data.ByteString.Lazy.Char8 as L
import Control.Monad (liftM)
import System.Environment (getArgs)
import Prelude hiding (takeWhile)
import qualified Data.Map as M
import Data.List (foldl', sortBy)
import Text.Printf (printf)
import Data.Maybe (fromMaybe)

type Command = String

data LogLine = LogLine {
    getIP     :: S.ByteString,
    getIdent  :: S.ByteString,
    getUser   :: S.ByteString,
    getDate   :: S.ByteString,
    getReq    :: S.ByteString,
    getStatus :: S.ByteString,
    getBytes  :: S.ByteString,
    getPath   :: S.ByteString,
    getUA     :: S.ByteString
} deriving (Ord, Show, Eq)

quote, lbrack, rbrack, space :: Parser Char
quote  = satisfy (== '\"')
lbrack = satisfy (== '[')
rbrack = satisfy (== ']')
space  = satisfy (== ' ')

quotedVal :: Parser S.ByteString
quotedVal = do
    quote
    res <- takeTill (== '\"')
    quote
    return res

bracketedVal :: Parser S.ByteString
bracketedVal = do
    lbrack
    res <- takeTill (== ']')
    rbrack
    return res

val :: Parser S.ByteString
val = takeTill (== ' ')

line :: Parser LogLine
l    ine = do
    ip <- val
    space
    identity <- val
    space
    user <- val
    space
    date <- bracketedVal
    space
    req <- quotedVal
    space
    status <- val
    space
    bytes <- val
    (path,ua) <- option ("","") combined
    return $ LogLine ip identity user date req status bytes path ua

combined :: Parser (S.ByteString,S.ByteString)
combined = do
    space
    path <- quotedVal
    space
    ua <- quotedVal
    return (path,ua)

countBytes :: [L.ByteString] -> Int
countBytes = foldl' count 0
    where
        count acc l = case AL.maybeResult $ AL.parse line l of
            Just x  -> (acc +) . maybe 0 fst . S.readInt . getBytes $ x
            Nothing -> acc

countIPs :: [L.ByteString] -> M.Map S.ByteString Int
countIPs = foldl' count M.empty
    where
        count acc l = case AL.maybeResult $ AL.parse line l of
            Just x -> M.insertWith' (+) (getIP x) 1 acc
            Nothing -> acc

---------------------------------------------------------------------------------

main :: IO ()
main = do
  [cmd,path] <- getArgs
  dispatch cmd path

pretty :: Show a => Int -> (a, Int) -> String
pretty i (bs, n) = printf "%d: %s, %d" i (show bs) n

dispatch :: Command -> FilePath -> IO ()
dispatch cmd path = action path
    where
        action = fromMaybe err (lookup cmd actions)
        err    = printf "Error: %s is not a valid command." cmd

actions :: [(Command, FilePath -> IO ())]
actions = [("bytes", countTotalBytes)
          ,("ips",  topListIP)]

countTotalBytes :: FilePath -> IO ()
countTotalBytes path = print . countBytes . L.lines =<< L.readFile path

topListIP :: FilePath -> IO ()
topListIP path = do
    f <- liftM L.lines $ L.readFile path
    let mostPopular (_,a) (_,b) = compare b a
        m = countIPs f
    mapM_ putStrLn . zipWith pretty [1..] . take 10 . sortBy mostPopular . M.toList $ m

Редактировать:

Добавление + RTS -A16M снизило GC до 20%. Использование памяти конечно же без изменений.


person Johanna Larsson    schedule 23.06.2011    source источник
comment
Не решение, но foldl' над накапливающейся картой - пустая трата. Просто используйте обычный foldl.   -  person John L    schedule 23.06.2011
comment
@John L, вы совершенно правы, быстрый тест не показывает разницы в скорости, GC или использовании памяти между foldl и foldl 'в этом случае.   -  person Johanna Larsson    schedule 23.06.2011


Ответы (2)


Предлагаю внести в код следующие изменения:

@@ -1,4 +1,4 @@
-{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE BangPatterns, OverloadedStrings #-}

 module Main where

@@ -9,7 +9,7 @@
 import Control.Monad (liftM)
 import System.Environment (getArgs)
 import Prelude hiding (takeWhile)
-import qualified Data.Map as M
+import qualified Data.HashMap.Strict as M
 import Data.List (foldl', sortBy)
 import Text.Printf (printf)
 import Data.Maybe (fromMaybe)
@@ -17,15 +17,15 @@
 type Command = String

 data LogLine = LogLine {
-    getIP     :: S.ByteString,
-    getIdent  :: S.ByteString,
-    getUser   :: S.ByteString,
-    getDate   :: S.ByteString,
-    getReq    :: S.ByteString,
-    getStatus :: S.ByteString,
-    getBytes  :: S.ByteString,
-    getPath   :: S.ByteString,
-    getUA     :: S.ByteString
+    getIP     :: !S.ByteString,
+    getIdent  :: !S.ByteString,
+    getUser   :: !S.ByteString,
+    getDate   :: !S.ByteString,
+    getReq    :: !S.ByteString,
+    getStatus :: !S.ByteString,
+    getBytes  :: !S.ByteString,
+    getPath   :: !S.ByteString,
+    getUA     :: !S.ByteString
 } deriving (Ord, Show, Eq)

 quote, lbrack, rbrack, space :: Parser Char
@@ -39,14 +39,14 @@
     quote
     res <- takeTill (== '\"')
     quote
-    return res
+    return $! res

 bracketedVal :: Parser S.ByteString
 bracketedVal = do
     lbrack
     res <- takeTill (== ']')
     rbrack
-    return res
+    return $! res

 val :: Parser S.ByteString
 val = takeTill (== ' ')
@@ -67,14 +67,14 @@
     space
     bytes <- val
     (path,ua) <- option ("","") combined
-    return $ LogLine ip identity user date req status bytes path ua
+    return $! LogLine ip identity user date req status bytes path ua

 combined :: Parser (S.ByteString,S.ByteString)
 combined = do
     space
-    path <- quotedVal
+    !path <- quotedVal
     space
-    ua <- quotedVal
+    !ua <- quotedVal
     return (path,ua)

 countBytes :: [L.ByteString] -> Int
@@ -84,11 +84,11 @@
             Just x  -> (acc +) . maybe 0 fst . S.readInt . getBytes $ x
             Nothing -> acc

-countIPs :: [L.ByteString] -> M.Map S.ByteString Int
+countIPs :: [L.ByteString] -> M.HashMap S.ByteString Int
 countIPs = foldl' count M.empty
     where
         count acc l = case AL.maybeResult $ AL.parse line l of
-            Just x -> M.insertWith' (+) (getIP x) 1 acc
+            Just x -> M.insertWith (+) (getIP x) 1 acc
             Nothing -> acc

 ---------------------------------------------------------------------------------

Я сделал поля LogLine строгими, чтобы они не содержали thunks, относящиеся к выражениям, связанным с синтаксическим анализом. Это хорошая практика - делать поля строгими, если вам действительно не нужно, чтобы они были ленивыми.

Я позаботился о том, чтобы результат синтаксического анализа был создан как можно скорее (это $! часть изменения), а также чтобы избежать задержки синтаксического анализа до тех пор, пока вы действительно не проверите отдельные поля LogLine.

Наконец, я переключился на улучшенную структуру данных HashMap из пакета неупорядоченных контейнеров. Обратите внимание, что все функции в Data.HashMap.Strict являются строгими по значению, что означает, что мы можем использовать простой вариант insertWith.

Обратите внимание, что взятие подстроки ByteString заставляет исходную строку сохраняться в памяти из-за совместного использования базового хранилища (это то же самое, что и для String в Java). Если вы хотите убедиться, что лишняя память не сохраняется, используйте функцию copy из пакета bytestring. Вы можете попытаться вызвать copy по результату (getIP x) и посмотреть, имеет ли это значение. Компромисс здесь заключается в использовании дополнительных вычислений для копирования строки в обмен на меньшее использование пространства.

Обратите внимание, что использование -A<high number>, как правило, улучшает производительность коротко работающих программ (т. Е. Тестов производительности), но не обязательно для реальных программ. То же самое и с -H. По крайней мере, более высокое значение -H (например, 1 ГБ) не повредит производительности вашей программы.

person tibbe    schedule 23.06.2011
comment
Используя (просто) копию и -A16M, скрипт теперь работает с производительностью 85% и достигает пика при использовании памяти 300 МБ (по сравнению с предыдущими 1,3 ГБ). Замедление с 40 до 50 секунд на разборе этого файла с 5 миллионами строк, на котором я его тестировал. Я проверим и другие твои предложения! - person Johanna Larsson; 24.06.2011
comment
Я бы порекомендовал попробовать -H (с 512M-1G или около того) и уменьшить -A, чтобы он соответствовал размеру вашего кэша L2. - person tibbe; 24.06.2011
comment
-A2M -H1G дает мне ~ 53 с (с кешем L2 2 МБ). Как ни странно, -A16M дает мне 40 секунд, а -A50M 36 секунд. Также я получил повышение производительности от перехода на строгую HashMap и еще одно повышение, когда я снова переключился на хэш-таблицы Грегори Коллина. - person Johanna Larsson; 24.06.2011
comment
Если вы увеличите -A достаточно, сборщик мусора никогда не будет работать, так как до завершения программы питомник никогда не будет полностью заполнен. В длительных программах высокое значение -A часто плохо, но в тестах оно иногда значительно улучшает ситуацию. - person tibbe; 27.06.2011

Самым очевидным моментом является то, что ваш первый скрипт может выбросить данные, как только он их увидит, тогда как второй должен удерживать все, что он видит. Следовательно, вы ожидаете, что второй скрипт займет как минимум O (N) памяти, тогда как первый может работать в постоянном пространстве.

Вы пробовали работать с включенным профилированием кучи? Я мог бы немного поработать там, где в вашем коде, скорее всего, произойдут избыточные выделения, но ничто не заменит жесткие данные.

Я бы сам с подозрением смотрел на вызовы Data.Map.insertWith, поскольку каждый из них отображает часть существующей избыточной карты в соответствии с требованиями и требует копирования и перебалансировки, но с моей стороны это чистая догадка. Если виноваты вызовы insertWith ', то, поскольку вам не нужны промежуточные записи Map, может быстрее построить всю карту за один проход (без каких-либо приращений для подсчета IP-адресов), а затем выполнить второй проход для подсчета. Таким образом, вы не будете тратить время на перебалансировку карты. Вы также можете воспользоваться тем фактом, что ваш ключевой тип данных соответствует Int (ну, это так, если это хотя бы адрес IPv4), и вместо этого использовать Data.IntMap, который имеет гораздо меньшие накладные расходы на память.

person Phil Armstrong    schedule 23.06.2011
comment
Не совсем! Он анализирует строку и сохраняет только небольшую часть - IP-адрес. Размер исходного проанализированного файла составляет почти 1,3 ГБ, поэтому большая часть его в любом случае блокируется в памяти! Но я согласен, insertWith - наиболее вероятный преступник. - person Johanna Larsson; 23.06.2011
comment
Это сообщение из списка новичков Haskell web.archiveorange.com/archive/v/aHum2LrqyulnPSegisUW отвечает на аналогичный вопрос. К сожалению, я не думаю, что предложенное там решение поможет решить вашу проблему, поскольку у вас другие требования! - person Phil Armstrong; 23.06.2011
comment
Я совершаю ту же ошибку, цитирую: складывать и вставлять на карту - плохая идея! Постараюсь увеличить площадь выделения. - person Johanna Larsson; 23.06.2011
comment
Это правда. Я предполагаю, что Data.Map - это просто неправильный тип данных для этого приложения: он слишком общий и предлагает набор гарантий, за которые вы платите, но которые на самом деле не нужны. Существует библиотека массивов Джуди, которая, вероятно, даст гораздо лучшую производительность за счет переноса большего количества вашего кода в монаду ввода-вывода! - person Phil Armstrong; 23.06.2011
comment
Я прочитал ссылку выше, но не могу понять, почему складывать и вставлять на карту - плохая идея. Может ли кто-нибудь предоставить ссылку, объясняющую это, для таких медлительных людей, как я? Или мне задать вопрос? Спасибо! - person Tim Perry; 23.06.2011
comment
Поскольку foldr - это ленивый Тим, вы в конечном итоге создаете цепочку вызовов insertWith в стеке, глубина которой равна количеству элементов на карте. - person Phil Armstrong; 07.07.2011
comment
См. Этот ответ для визуального описания происходящего: stackoverflow.com/questions/4008473/ - person Phil Armstrong; 07.07.2011