Я использую haskell для построчной обработки данных, т.е. задач, где можно применить sed
, awk
и подобные инструменты. В качестве простого примера давайте добавим 000
к каждой строке из стандартного ввода.
У меня есть три альтернативных способа выполнить задачу:
- Ленивый ввод-вывод с ленивыми
ByteString
s - Трубопровод на основе линии.
- Кондуит на основе фрагментов с чистой строгой обработкой
ByteString
внутри.
example.hs
:
{-# LANGUAGE NoImplicitPrelude #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE FlexibleContexts #-}
import ClassyPrelude.Conduit
import qualified Data.ByteString.Char8 as B8
import qualified Data.ByteString.Lazy.Char8 as BL8
import qualified Data.Conduit.Binary as CB
main = do
[arg] <- getArgs
case arg of
"lazy" -> BL8.getContents >>= BL8.putStr . BL8.unlines . map ("000" ++) . BL8.lines
"lines" -> runConduitRes $ stdinC .| CB.lines .|
mapC ("000" ++) .| mapC (`snoc` 10) .| stdoutC
"chunks" -> runConduitRes $ stdinC .| lineChunksC .|
mapC (B8.unlines . (map ("000" ++)) . B8.lines) .| stdoutC
lineChunksC :: Monad m => Conduit ByteString m ByteString
lineChunksC = await >>= maybe (return ()) go
where
go acc = if
| Just (_, 10) <- unsnoc acc -> yield acc >> lineChunksC
| otherwise -> await >>= maybe (yield acc) (go' . breakAfterEOL)
where
go' (this, next) = let acc' = acc ++ this in if null next then go acc' else yield acc' >> go next
breakAfterEOL :: ByteString -> (ByteString, ByteString)
breakAfterEOL = uncurry (\x -> maybe (x, "") (first (snoc x)) . uncons) . break (== 10)
$ stack ghc --package={classy-prelude-conduit,conduit-extra} -- -O2 example.hs -o example $ for cmd in lazy lines chunks; do echo $cmd; time -p seq 10000000 | ./example $cmd > /dev/null; echo; done lazy real 2.99 user 3.06 sys 0.07 lines real 3.30 user 3.36 sys 0.06 chunks real 1.83 user 1.95 sys 0.06
(Результаты одинаковы для нескольких прогонов, а также для строк с несколькими числами).
Таким образом, chunks
в 1,6 раза быстрее, чем lines
, что немного быстрее, чем lazy
. Это означает, что конвейеры могут быть быстрее, чем простые строки байтов, но накладные расходы конвейеров слишком велики, когда вы разбиваете фрагменты на короткие строки.
Что мне не нравится в подходе chunks
, так это то, что он смешивает как проводник, так и чистый мир, и это затрудняет его использование для более сложных задач.
Вопрос в том, не упустил ли я простое и элегантное решение, которое позволило бы мне писать эффективный код так же, как при подходе lines
?
EDIT1: по предложению @Michael я объединил два mapC
в один mapC (("000" ++). (
snoc10))
в решении lines
, чтобы число труб (.|
) было одинаковым между lines
и chunks
. Это заставило его работать немного лучше (с 3,3 с до 2,8 с), но все же значительно медленнее, чем chunks
.
Также я попробовал более старый Conduit.Binary.lines
, который Майкл предложил в комментариях, и он также немного повышает производительность, примерно на 0,1 с.
EDIT2: исправлено lineChunksC
, поэтому оно работает с очень маленькими фрагментами, например.
> runConduitPure $ yield ("\nr\n\n"::ByteString) .| concatC .| mapC singleton .| lineChunksC .| sinkList
["\n","r\n","\n"]
mapC (`snoc` 10)
? - person chepner   schedule 29.10.2016'\n'
, поэтому он добавляет символ новой строки обратно к каждой строке. - person modular   schedule 29.10.2016stdinC' = (stdinC :: Source (ResourceT IO) B.ByteString) .| concatC .| mapC B.singleton
Это не влияет на семантикуstdinC
, но нарушает эту функцию, но не функцию трубопроводных линий. - person Michael   schedule 29.10.2016chunks
выделяет столько промежуточных цепочек байтов, сколько байтов в строке. Рационально, когда вы ожидаете один или два фрагмента в строке. Это, конечно, достаточно типично. - person Michael   schedule 29.10.2016lines
обладал этим свойством; см. этот патч github.com/snoyberg/conduit/pull/209 Возможно, старая версия была быстрее для входных данных, подобных вашему, где у нас есть тонна коротких строк, состоящих из одного фрагмента. - person Michael   schedule 29.10.2016CB.lines
работает лучше для меня (ускоряет работу на ~ 5%), но в любом случае большая часть времени тратится на конвейер (предохранитель,>>=
). - person modular   schedule 29.10.2016lineChunksC
? Вполне вероятно, я просто писал для решенияchunks
и ни на чем другом не тестировал. - person modular   schedule 29.10.2016mapC (("000" ++). (`snoc` 10))
вместоmapC ("000" ++) .| mapC (`snoc` 10)
. Это делается неявно другим. - person Michael   schedule 29.10.2016.|
делает программу более сложной, если только они не сливаются. Вашchunks
использует три, но вашlines
использует четыре. Это не имеет ничего общего с функцией линий. - person Michael   schedule 29.10.2016mapC
повышает производительность, но не радикально, поэтому при построчной обработке конвейера по-прежнему наблюдается серьезная потеря производительности. - person modular   schedule 30.10.2016lineChunksC
, это будет невозможно проверить. Попробуйте, например.yield ("\nr\n\n"::B.ByteString) =$= concatC =$= mapC B.singleton
вместоstdinC
. Это важно для бенчмаркинга. - person Michael   schedule 31.10.2016ByteString.lines
, но, как и ожидалось, если я передам ему файл из 10 миллионов t без разрывов строки, он будет намного медленнее sprunge.us/APeK Если я даю файл в однобайтовых фрагментах char за char, он не завершается через 5 минут, так как он выделяет 10M отдельных bytestrings, в то время как другой делается за 5 секунд. Я думаю, что это квадратично по количеству фрагментов в строке. Вот мой источник sprunge.us/gVQG - person Michael   schedule 31.10.2016