在不断努力有效地摆弄比特(例如,参见这个SO question)中,最新的挑战是比特的有效流式传输和消耗。
作为第一个简单任务,我选择在由/dev/urandom
. 一个典型的咒语是head -c 1000000 </dev/urandom | my-exe
。实际目标是流式传输比特并解码Elias 伽马码,例如,即不是字节块或其倍数的代码。
对于这种可变长度的代码,最好使用take
, takeWhile
,group
等语言进行列表操作。由于 aBitStream.take
实际上会消耗部分双流,因此某些 monad 可能会发挥作用。
明显的起点是来自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
这种幼稚的实现确实慢了一个数量级。
分析显示分配了相当多的内存(大约 3GB 用于解析 1MB 的输入)。不过,没有观察到大规模的空间泄漏。
从这里我开始四处寻找:
- 有一个
bitstream
包承诺“具有半自动流融合的快速、打包、严格的比特流(即布尔列表)。 ”。vector
不幸的是,它与当前的软件包不是最新的,请参阅此处了解详细信息。 - 接下来,我调查
streaming
. 我不太明白为什么我应该需要“有效”的流来让一些 monad 发挥作用——至少在我开始与所提出的任务相反之前,即编码和将比特流写入文件。 - 怎么
fold
样ByteString
?我必须引入状态来跟踪消耗的位。这不是很好的take
,takeWhile
,group
, 等语言。
现在我不太确定该去哪里。
更新:
我想出了如何使用streaming
and来做到这一点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
这将测试您对标准输入几千字节输入之外的任何内容的耐心。分析器说它在 和 中花费了大量时间(输入大小的二次方Streaming.Internal.>>=.loop
)Data.Functor.Of.fmap
。我不太确定第一个是什么,但fmap
表明(?)这些杂耍对Of a b
我们没有任何好处,而且因为我们在 IO monad 中,它不能被优化掉。
我这里也有相当于字节加法器的流:SumBytesStream.hs
,它比简单的惰性实现稍慢ByteString
,但仍然不错。由于streaming-bytestring
被宣布为“ bytestring io done right ” 我期望更好。那我可能做得不对。
在任何情况下,所有这些位计算都不应该发生在 IO monad 中。但是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 相当的性能。使用的构造称为“Church 编码列表”,请参阅此SO 答案或 Haskell Wiki on rank-N types。
源文件:
所有源文件都可以在github上找到。具有运行实验和分析的Makefile
所有各种目标。默认make
将只构建所有内容(首先创建一个bin/
目录!),然后make time
对longest-seq
可执行文件进行计时。C 可执行文件会-c
附加一个以区分它们。