В постоянном стремлении эффективно сражаться с битами (например, см. Этот вопрос SO) самой новой задачей является эффективная потоковая передача и потребление бит.
В качестве первой простой задачи я выбираю найти самую длинную последовательность идентичных бит в битовом потоке, генерируемом /dev/urandom
. Типичным заклинанием будет head -c 1000000 </dev/urandom | my-exe
head -c 1000000 </dev/urandom | my-exe
. Фактическая цель состоит в том, чтобы потоковые биты и декодировать гамма-код Elias, например, то есть коды, которые не являются кусками байтов или их кратных.
Для таких кодов переменной длины приятно иметь язык take
, takeWhile
, group
и т.д. Для манипулирования списками. Так как BitStream.take
самом деле потребляет часть биста, возможно, вступает в игру какая-то монада.
Очевидной отправной точкой является ленивая байтовая строка из Data.ByteString.Lazy
.
A. Подсчет байтов
Эта очень простая программа Haskell выполняет наравне с программой C, как и следовало ожидать.
import qualified Data.ByteString.Lazy as BSL
main :: IO ()
main = do
bs <- BSL.getContents
print $ BSL.length bs
B. Добавление байтов
Как только я начну использовать unpack
все должно стать хуже.
main = do
bs <- BSL.getContents
print $ sum $ BSL.unpack bs
Удивительно, что Haskell и C показывают почти такую же производительность.
C. Самая длинная последовательность идентичных бит
В качестве первой нетривиальной задачи самая длинная последовательность идентичных бит может быть найдена следующим образом:
module Main where
import Data.Bits (shiftR, (.&.))
import qualified Data.ByteString.Lazy as BSL
import Data.List (group)
import Data.Word8 (Word8)
splitByte :: Word8 -> [Bool]
splitByte w = Prelude.map (\i-> (w 'shiftR' i) .&. 1 == 1) [0..7]
bitStream :: BSL.ByteString -> [Bool]
bitStream bs = concat $ map splitByte (BSL.unpack bs)
main :: IO ()
main = do
bs <- BSL.getContents
print $ maximum $ length <$> (group $ bitStream bs)
Лесткая байтовая трансформация преобразуется в список [Word8]
а затем, используя сдвиги, каждое Word
разбивается на биты, в результате чего появляется список [Bool]
. Этот список списков затем сглаживается с помощью concat
. Получив (ленивый) список Bool
, используйте group
чтобы разбить список на последовательности одинаковых битов, а затем отобразить length
над ним. Наконец, maximum
дает желаемый результат. Довольно просто, но не очень быстро:
# C
real 0m0.606s
# Haskell
real 0m6.062s
Эта наивная реализация на порядок медленнее.
Профилирование показывает, что выделяется довольно много памяти (около 3 ГБ для разбора 1 МБ ввода). Тем не менее, нет большой утечки пространства.
Отсюда я начинаю ковырять:
- Существует
bitstream
пакет, который обещает "быстро, упакованы, жесткие битовые потоки (т.е. список из BOOLS) с полуавтоматическим слиянием потока.". К сожалению, он не обновляется с текущим пакетомvector
, см. Здесь подробности. - Затем я исследую
streaming
. Я не совсем понимаю, зачем мне нужно "эффектное" потоковое вещание, которое приносит какую-нибудь монаду в игру - по крайней мере, до тех пор, пока я не начну с обратной задачи поставленной задачи, то есть кодирования и записи битовых потоков в файл. - Как просто
fold
-ing надByteString
? Я должен был бы ввести состояние, чтобы отслеживать потребляемые биты. Это не совсем приятноеtake
,takeWhile
,group
и т.д. Язык, который желателен.
И теперь я не совсем уверен, куда идти.
Обновление:
Я понял, как это сделать с streaming
и streaming-bytestring
. Я, вероятно, не делаю этого правильно, потому что результат катастрофически плох.
import Data.Bits (shiftR, (.&.))
import qualified Data.ByteString.Streaming as BSS
import Data.Word8 (Word8)
import qualified Streaming as S
import Streaming.Prelude (Of, Stream)
import qualified Streaming.Prelude as S
splitByte :: Word8 -> [Bool]
splitByte w = (\i-> (w 'shiftR' i) .&. 1 == 1) <$> [0..7]
bitStream :: Monad m => Stream (Of Word8) m () -> Stream (Of Bool) m ()
bitStream s = S.concat $ S.map splitByte s
main :: IO ()
main = do
let bs = BSS.unpack BSS.getContents :: Stream (Of Word8) IO ()
gs = S.group $ bitStream bs :: Stream (Stream (Of Bool) IO) IO ()
maxLen <- S.maximum $ S.mapped S.length gs
print $ S.fst' maxLen
Это проверит ваше терпение с чем-то, кроме нескольких тысяч байтов ввода от stdin. Профайлер говорит, что он проводит безумное количество времени (квадратичное по размеру ввода) в Streaming.Internal.>>=.loop
и Data.Functor.Of.fmap
. Я не совсем уверен, что такое первый, но fmap
указывает (?), Что жонглирование этих Of ab
не приносит нам никакой пользы, и поскольку мы находимся в монаде IO, его нельзя оптимизировать.
У меня также есть потоковый эквивалент байтового сумматора : SumBytesStream.hs
, который немного медленнее, чем простая ленивая реализация ByteString
, но по-прежнему приличная. Поскольку streaming-bytestring
провозглашается как "bytestring io done right", я ожидал лучшего. Тогда я, вероятно, не сделаю это правильно.
В любом случае, все эти бит -c не должны происходить в монаде IO. Но BSS.getContents
заставляет меня в монаду IO, потому что getContents :: MonadIO m => ByteString m()
и нет выхода.
Обновление 2
Следуя совету @dfeuer, я использовал streaming
пакет в master @HEAD. Вот результат.
longest-seq-c 0m0.747s (C)
longest-seq 0m8.190s (Haskell ByteString)
longest-seq-stream 0m13.946s (Haskell streaming-bytestring)
Проблема O (n ^ 2) Streaming.concat
решена, но мы все еще не приближаемся к эталону C.
Обновление 3
Решение Cirdec дает производительность по аналогии с C. Используемая конструкция называется "Церковно-кодированные списки", см. Этот ответ SO или Haskell Wiki по типам ранга-N.
Исходные файлы:
Все исходные файлы можно найти в github. В Makefile
есть все различные цели для запуска экспериментов и профилирования. По умолчанию make
будет просто создавать все (сначала создайте каталог bin/
!), А затем make time
для выполнения самых longest-seq
исполняемых файлов. В исполняемых файлах C добавляется -c
чтобы различать их.