Flujos de bits eficientes en Haskell
n un esfuerzo continuo por manipular eficientemente los bits (por ejemplo, vea esteSO pregunta) el desafío más nuevo es la transmisión eficiente y el consumo de bits.
omo una primera tarea simple, elijo encontrar la secuencia más larga de bits idénticos en un flujo de bits generado por/dev/urandom
. Un encantamiento típico seríahead -c 1000000 </dev/urandom | my-exe
. El objetivo real es transmitir bits y decodificar unaElias gamma code, por ejemplo, códigos que no son fragmentos de bytes o múltiplos de los mismos.
Para tales códigos de longitud variable, es bueno tener eltake
, takeWhile
, group
, etc. lenguaje para la manipulación de listas. Desde unBitStream.take
en realidad consumiría parte del bistream que alguna mónada probablemente entraría en juego.
l punto de partida obvio es la cadena de bytes perezosa deData.ByteString.Lazy
.
UN. Contando bytes
Este programa muy simple de Haskell funciona a la par con un programa en C, como es de esperar.
import qualified Data.ByteString.Lazy as BSL
main :: IO ()
main = do
bs <- BSL.getContents
print $ BSL.length bs
SI. Añadiendo bytes
Una vez que empiezo a usarunpack
las cosas deberían empeorar.
main = do
bs <- BSL.getContents
print $ sum $ BSL.unpack bs
orprendentemente, Haskell y C muestran casi el mismo rendimiento.
C. Secuencia más larga de bits idénticos
omo primera tarea no trivial, la secuencia más larga de bits idénticos se puede encontrar así:
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 <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)
gt; (group $ bitStream bs)
La cadena de bytes perezosa se convierte en una lista[Word8]
y luego, usando turnos, cadaWord
se divide en bits, lo que da como resultado una lista[Bool]
. Esta lista de listas se aplana conconcat
. Habiendo obtenido una lista (perezosa) deBool
, utilizargroup
para dividir la lista en secuencias de bits idénticos y luego asignarlength
encima de eso. Finalmentemaximum
da el resultado deseado. Bastante simple, pero no muy rápido:
# C
real 0m0.606s
# Haskell
real 0m6.062s
Esta ingenua implementación es exactamente un orden de magnitud más lenta.
Profiling muestra que se asigna una gran cantidad de memoria (aproximadamente 3 GB para analizar 1 MB de entrada). Sin embargo, no se observa una fuga espacial masiva.
Desde aquí empiezo a hurgar:
Hay unbitstream
paquete que promete "Secuencias de bits estrictas, rápidas y compactas (es decir, lista de Bools) con fusión de secuencia semiautomática. ". Desafortunadamente no está actualizado con el @ actuvector
paquete, veraqu para detallesNext, investigostreaming
. No entiendo por qué debería necesitar una transmisión 'efectiva' que ponga en juego algo de mónada, al menos hasta que comience con el reverso de la tarea planteada, es decir, codificar y escribir secuencias de bits en el archivo. ¿Qué tal solofold
-ing sobre elByteString
? Tendría que introducir el estado para realizar un seguimiento de los bits consumidos. Esa no es la buenatake
, takeWhile
, group
, etc. lenguaje que es deseable.Y ahora no estoy muy seguro de a dónde ir.
Actualiza:
He descubierto cómo hacer esto constreaming
ystreaming-bytestring
. Probablemente no estoy haciendo esto bien porque el resultado es catastróficamente malo.
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) <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
gt; [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
Esto pondrá a prueba su paciencia con cualquier cosa más allá de unos pocos miles de bytes de entrada de stdin. El generador de perfiles dice que gasta una cantidad de tiempo increíble (cuadrática en el tamaño de entrada) enStreaming.Internal.>>=.loop
yData.Functor.Of.fmap
. No estoy muy seguro de cuál es el primero, pero elfmap
indica (?) que el malabarismo de estosOf a b
no nos está haciendo ningún bien y debido a que estamos en la mónada IO, no se puede optimizar.
También tengo el equivalente de transmisión del sumador de bytesaquí:SumBytesStream.hs
, que es un poco más lento que el simple vagoByteString
implementación, pero sigue siendo decente. Ya questreaming-bytestring
es proclamado ser - estar "bytestring io hecho bien "Esperaba algo mejor. Probablemente no lo estoy haciendo bien, entonces.
En cualquier caso, todos estos cálculos de bits no deberían estar sucediendo en la mónada IO. PeroBSS.getContents
me obliga a entrar en la mónada IO porquegetContents :: MonadIO m => ByteString m ()
y no hay salida.
Update 2
Siguiendo los consejos de @dfeuer utilicé lastreaming
paquete en master @ HEAD. Aquí está el resultado.
longest-seq-c 0m0.747s (C)
longest-seq 0m8.190s (Haskell ByteString)
longest-seq-stream 0m13.946s (Haskell streaming-bytestring)
El problema O (n ^ 2) deStreaming.concat
está resuelto, pero todavía no nos estamos acercando al punto de referencia C.
Update 3
a solución de @ Cirdec produce un rendimiento a la par con C. La construcción que se utiliza se llama "listas codificadas de la Iglesia", consulte estaPues contest o el Wiki de Haskell en tipos de rango N.
Archivos fuente
Todos los archivos de origen se pueden encontrar en github. LosMakefile
tiene todos los objetivos para ejecutar los experimentos y la creación de perfiles. El valor por defectomake
solo construirá todo (crea unbin/
¡directorio primero!) y luegomake time
hará el tiempo en ellongest-seq
ejecutables. Los ejecutables de C obtienen una-c
añadido para distinguirlos.