4

There seems to be some impediment to efficient lazy ByteString generation by recursion. To demonstrate this, the chosen task is to make a lazy random ByteString. (Random number generation is just a reasonably meaningful operation, i.e. a placeholder for any other recursion that might be of interest.)

Here are two attempts to create a random lazy ByteString of fixed length n. They allocate huge amounts of heap. First some imports:

import qualified Data.ByteString.Lazy as BSL
import Data.Word8
import System.Random

Now the function that uses cons:

lazyRandomByteString1 :: Int -> StdGen -> BSL.ByteString
lazyRandomByteString1 n g = fst3 $ iter (BSL.empty, n, g) where
    fst3 (a, _, _) = a
    iter (bs', n', g') =
        if n' == 0 then (bs', 0, g')
        else iter (w `BSL.cons` bs', n'-1, g'') where
            (w, g'') = random g' :: (Word8, StdGen)

The same, just using unfoldr is shorter, but almost as bad as the above:

lazyRandomByteString2 :: Int -> StdGen -> BSL.ByteString
lazyRandomByteString2 n g = BSL.unfoldr f (n, g) where
    f :: (Int, StdGen) -> (Int, StdGen)
    f (n', g') =
        if n' == 0 then Nothing
        else Just (w, (n'-1, g'')) where
            (w, g'') = random g' :: (Word8, StdGen)

Within what's provided by Data.ByteString.Lazy these are all the available options to create ByteStrings by recursion.

Next, turn to Data.ByteString.Lazy.Builder, it was built to build lazy ByteStrings, surely this must be more efficient:

import Data.ByteString.Lazy.Builder (Builder, toLazyByteString, word8)

lazyRandomByteString3 :: Int -> StdGen -> BSL.ByteString
lazyRandomByteString3 n g = toLazyByteString builder where
    builder :: Builder
    builder = fst3 $ iter (mempty, n, g) where
        fst3 (a, _, _) = a
        iter :: (Builder, Int, StdGen) -> (Builder, Int, StdGen)
        iter (b, n', g') =
            if n' == 0 then (b, 0, g')
            else iter (b <> (word8 w), n'-1, g'') where
                (w, g'') = random g' :: (Word8, StdGen)

But it isn't.

Builder really should be able to do this efficiently, shouldn't it? What is wrong with lazyRandomByteString3?

The source code is on github.

mcmayer
  • 1,931
  • 12
  • 22
  • How are you compiling and testing this? – Thomas M. DuBuisson Jul 17 '18 at 15:21
  • Have a look at the github repo. I use the profiler. – mcmayer Jul 17 '18 at 16:05
  • 2
    When I compile & run this, I see `157,396,032 bytes maximum residency` which seems pretty reasonable for a million-byte array. The profile shows that 78% of allocations are in `random`. If you want to compare your 3 recursion patterns, it might help to use a less-expensive placeholder function. – bergey Jul 17 '18 at 17:30
  • So you are looking at the total allocation (as indicated by your 'grep') instead of the maximum residency? Are you mistaking this for the maximum residency? – Thomas M. DuBuisson Jul 17 '18 at 19:37
  • ok, total alloc may not be a good benchmark. But it's too high, anyway. I expect some kind of fusion happening, and much faster runtimes. I'll try with a different placeholder function. – mcmayer Jul 17 '18 at 20:32
  • 1
    What is the basis for your expectation? You're generating 1 byte for every step of StdGen which consumes around 152 bytes each step for 1G allocated just for the RNG prior to optimizations (which are inhibited by profiling). – Thomas M. DuBuisson Jul 17 '18 at 20:39
  • I swapped in a dumb old RNG, and I enforced some strictness, but that makes no difference. When you run this with 1G bytes, no profiler, you'll see that it will exhaust your RAM. In the end, this should use 32kB (chunk size, I believe) plus some overhead. – mcmayer Jul 18 '18 at 04:24

0 Answers0