Skip to content

Commit ed7a9d4

Browse files
committed
Data.ByteString.Lazy.dropEnd: Use two-pointers technique (#629)
* Data.ByteString.Lazy.dropEnd: Use two-pointers technique This can be seen as using the input itself as an implicit queue; we formerly copied its chunks into an explicit queue. By writing the key logic as a polymorphic `splitAtEndFold`, it was easy to re-use it for `takeEnd`; the latter function should now operate in constant stack space and can release initial chunks of a very long input string sooner. * Fix a very silly bug, and strengthen tests ...so that a plain 'cabal test' finds the bug almost every try instead of finding it only every few dozen tries * Actually work around the poison instance (Some re-compilation check somewhere set a trap for me.) This also replaces fromIntegral with intToIndexTy in a few places. * Rewrite the poison instance using TypeError * Rename "bsL" -> "toSplit" and "bsR" -> "toScan" * Add basic benchmarks for lazy takeEnd/splitEnd According to these benchmarks, the new implementation for takeEnd is somewhat faster and the new implementation for dropEnd is roughly 3.5x to 4x as quick as its predecessor. (cherry picked from commit 2bbc97e)
1 parent 62d1835 commit ed7a9d4

File tree

7 files changed

+156
-133
lines changed

7 files changed

+156
-133
lines changed

Data/ByteString/Lazy.hs

Lines changed: 80 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
1-
{-# LANGUAGE BangPatterns #-}
21
{-# OPTIONS_GHC -fno-warn-incomplete-patterns #-}
32
{-# OPTIONS_HADDOCK prune #-}
43
{-# LANGUAGE Trustworthy #-}
54

5+
{-# LANGUAGE BangPatterns #-}
6+
{-# LANGUAGE TypeApplications #-}
7+
{-# LANGUAGE ScopedTypeVariables #-}
8+
69
-- |
710
-- Module : Data.ByteString.Lazy
811
-- Copyright : (c) Don Stewart 2006
@@ -237,9 +240,9 @@ import qualified Data.ByteString as P (ByteString) -- type name only
237240
import qualified Data.ByteString as S -- S for strict (hmm...)
238241
import qualified Data.ByteString.Internal.Type as S
239242
import qualified Data.ByteString.Unsafe as S
240-
import qualified Data.ByteString.Lazy.Internal.Deque as D
241243
import Data.ByteString.Lazy.Internal
242244

245+
import Control.Exception (assert)
243246
import Control.Monad (mplus)
244247
import Data.Word (Word8)
245248
import Data.Int (Int64)
@@ -790,15 +793,75 @@ take i cs0 = take' i cs0
790793
--
791794
-- @since 0.11.2.0
792795
takeEnd :: Int64 -> ByteString -> ByteString
793-
takeEnd i _ | i <= 0 = Empty
794-
takeEnd i cs0 = takeEnd' i cs0
795-
where takeEnd' 0 _ = Empty
796-
takeEnd' n cs =
797-
snd $ foldrChunks takeTuple (n,Empty) cs
798-
takeTuple _ (0, cs) = (0, cs)
799-
takeTuple c (n, cs)
800-
| n > fromIntegral (S.length c) = (n - fromIntegral (S.length c), Chunk c cs)
801-
| otherwise = (0, Chunk (S.takeEnd (fromIntegral n) c) cs)
796+
takeEnd i bs
797+
| i <= 0 = Empty
798+
| otherwise = splitAtEndFold (\_ res -> res) id i bs
799+
800+
-- | Helper function for implementing 'takeEnd' and 'dropEnd'
801+
splitAtEndFold
802+
:: forall result
803+
. (S.StrictByteString -> result -> result)
804+
-- ^ What to do when one chunk of output is ready
805+
-- (The StrictByteString will not be empty.)
806+
-> (ByteString -> result)
807+
-- ^ What to do when the split-point is reached
808+
-> Int64
809+
-- ^ Number of bytes to leave at the end (must be strictly positive)
810+
-> ByteString -- ^ Input ByteString
811+
-> result
812+
{-# INLINE splitAtEndFold #-}
813+
splitAtEndFold step end len bs0 = assert (len > 0) $ case bs0 of
814+
Empty -> end Empty
815+
Chunk c t -> goR len c t t
816+
where
817+
-- Idea: Keep two references into the input ByteString:
818+
-- "toSplit" tracks the current split point,
819+
-- "toScan" tracks the yet-unprocessed tail.
820+
-- When they are closer than "len" bytes apart, process more input. ("goR")
821+
-- When they are at least "len" bytes apart, produce more output. ("goL")
822+
-- We always have that "toScan" is a suffix of "toSplit",
823+
-- and "toSplit" is a suffix of the original input (bs0).
824+
goR :: Int64 -> S.StrictByteString -> ByteString -> ByteString -> result
825+
goR !undershoot nextOutput@(S.BS noFp noLen) toSplit toScan =
826+
assert (undershoot > 0) $
827+
-- INVARIANT: length toSplit == length toScan + len - undershoot
828+
-- (not 'assert'ed because that would break our laziness properties)
829+
case toScan of
830+
Empty
831+
| undershoot >= intToInt64 noLen
832+
-> end (Chunk nextOutput toSplit)
833+
| undershootW <- fromIntegral @Int64 @Int undershoot
834+
-- conversion Int64->Int is OK because 0 < undershoot < noLen
835+
, splitIndex <- noLen - undershootW
836+
, beforeSplit <- S.BS noFp splitIndex
837+
, afterSplit <- S.BS (noFp `S.plusForeignPtr` splitIndex) undershootW
838+
-> step beforeSplit $ end (Chunk afterSplit toSplit)
839+
840+
Chunk (S.BS _ cLen) newBsR
841+
| cLen64 <- intToInt64 cLen
842+
, undershoot > cLen64
843+
-> goR (undershoot - cLen64) nextOutput toSplit newBsR
844+
| undershootW <- fromIntegral @Int64 @Int undershoot
845+
-> step nextOutput $ goL (cLen - undershootW) toSplit newBsR
846+
847+
goL :: Int -> ByteString -> ByteString -> result
848+
goL !overshoot toSplit toScan =
849+
assert (overshoot >= 0) $
850+
-- INVARIANT: length toSplit == length toScan + len + intToInt64 overshoot
851+
-- (not 'assert'ed because that would break our laziness properties)
852+
case toSplit of
853+
Empty -> splitAtEndFoldInvariantFailed
854+
Chunk c@(S.BS _ cLen) newBsL
855+
| overshoot >= cLen
856+
-> step c $ goL (overshoot - cLen) newBsL toScan
857+
| otherwise
858+
-> goR (intToInt64 $ cLen - overshoot) c newBsL toScan
859+
860+
splitAtEndFoldInvariantFailed :: a
861+
-- See Note [Float error calls out of INLINABLE things] in D.B.Internal.Type
862+
splitAtEndFoldInvariantFailed =
863+
moduleError "splitAtEndFold"
864+
"internal error: toSplit not longer than toScan"
802865

803866
-- | /O(n\/c)/ 'drop' @n xs@ returns the suffix of @xs@ after the first @n@
804867
-- elements, or 'empty' if @n > 'length' xs@.
@@ -824,44 +887,9 @@ drop i cs0 = drop' i cs0
824887
--
825888
-- @since 0.11.2.0
826889
dropEnd :: Int64 -> ByteString -> ByteString
827-
dropEnd i p | i <= 0 = p
828-
dropEnd i p = go D.empty p
829-
where go :: D.Deque -> ByteString -> ByteString
830-
go deque (Chunk c cs)
831-
| D.byteLength deque < i = go (D.snoc c deque) cs
832-
| otherwise =
833-
let (output, deque') = getOutput empty (D.snoc c deque)
834-
in foldrChunks Chunk (go deque' cs) output
835-
go deque Empty = fromDeque $ dropEndBytes deque i
836-
837-
len c = fromIntegral (S.length c)
838-
839-
-- get a `ByteString` from all the front chunks of the accumulating deque
840-
-- for which we know they won't be dropped
841-
getOutput :: ByteString -> D.Deque -> (ByteString, D.Deque)
842-
getOutput out deque = case D.popFront deque of
843-
Nothing -> (reverseChunks out, deque)
844-
Just (x, deque') | D.byteLength deque' >= i ->
845-
getOutput (Chunk x out) deque'
846-
_ -> (reverseChunks out, deque)
847-
848-
-- reverse a `ByteString`s chunks, keeping all internal `S.StrictByteString`s
849-
-- unchanged
850-
reverseChunks = foldlChunks (flip Chunk) empty
851-
852-
-- drop n elements from the rear of the accumulating `deque`
853-
dropEndBytes :: D.Deque -> Int64 -> D.Deque
854-
dropEndBytes deque n = case D.popRear deque of
855-
Nothing -> deque
856-
Just (deque', x) | len x <= n -> dropEndBytes deque' (n - len x)
857-
| otherwise ->
858-
D.snoc (S.dropEnd (fromIntegral n) x) deque'
859-
860-
-- build a lazy ByteString from an accumulating `deque`
861-
fromDeque :: D.Deque -> ByteString
862-
fromDeque deque =
863-
List.foldr chunk Empty (D.front deque) `append`
864-
List.foldl' (flip chunk) Empty (D.rear deque)
890+
dropEnd i p
891+
| i <= 0 = p
892+
| otherwise = splitAtEndFold Chunk (const Empty) i p
865893

866894
-- | /O(n\/c)/ 'splitAt' @n xs@ is equivalent to @('take' n xs, 'drop' n xs)@.
867895
splitAt :: Int64 -> ByteString -> (ByteString, ByteString)
@@ -1688,6 +1716,9 @@ revNonEmptyChunks = List.foldl' (flip Chunk) Empty
16881716
revChunks :: [P.ByteString] -> ByteString
16891717
revChunks = List.foldl' (flip chunk) Empty
16901718

1719+
intToInt64 :: Int -> Int64
1720+
intToInt64 = fromIntegral @Int @Int64
1721+
16911722
-- $IOChunk
16921723
--
16931724
-- ⚠ Using lazy I\/O functions like 'readFile' or 'hGetContents'

Data/ByteString/Lazy/Internal/Deque.hs

Lines changed: 0 additions & 65 deletions
This file was deleted.

bench/BenchAll.hs

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ import Data.Semigroup
2020
import Data.String
2121
import Test.Tasty.Bench
2222
import Prelude hiding (words)
23+
import qualified Data.List as List
24+
import Control.DeepSeq
2325

2426
import qualified Data.ByteString as S
2527
import qualified Data.ByteString.Char8 as S8
@@ -99,9 +101,12 @@ lazyByteStringData = case S.splitAt (nRepl `div` 2) byteStringData of
99101

100102
{-# NOINLINE smallChunksData #-}
101103
smallChunksData :: L.ByteString
102-
smallChunksData
103-
= L.fromChunks [S.take sz (S.drop n byteStringData)
104-
| let sz = 48, n <- [0, sz .. S.length byteStringData]]
104+
smallChunksData = L.fromChunks $ List.unfoldr step (byteStringData, 1)
105+
where
106+
step (!s, !i)
107+
| S.null s = Nothing
108+
| otherwise = case S.splitAt i s of
109+
(!s1, !s2) -> Just (s1, (s2, i * 71 `mod` 97))
105110

106111
{-# NOINLINE byteStringChunksData #-}
107112
byteStringChunksData :: [S.ByteString]
@@ -419,6 +424,19 @@ main = do
419424
[ bench "strict" $ nf S.tails byteStringData
420425
, bench "lazy" $ nf L.tails lazyByteStringData
421426
]
427+
, bgroup "splitAtEnd (lazy)" $ let
428+
testSAE op = \bs -> [op i bs | i <- [0,5..L.length bs]] `deepseq` ()
429+
{-# INLINE testSAE #-}
430+
in
431+
[ bench "takeEnd" $
432+
nf (testSAE L.takeEnd) lazyByteStringData
433+
, bench "takeEnd (small chunks)" $
434+
nf (testSAE L.takeEnd) smallChunksData
435+
, bench "dropEnd" $
436+
nf (testSAE L.dropEnd) lazyByteStringData
437+
, bench "dropEnd (small chunks)" $
438+
nf (testSAE L.dropEnd) smallChunksData
439+
]
422440
, bgroup "sort" $ map (\s -> bench (S8.unpack s) $ nf S.sort s) sortInputs
423441
, bgroup "stimes" $ let st = stimes :: Int -> S.ByteString -> S.ByteString
424442
in

bytestring.cabal

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,6 @@ library
106106
Data.ByteString.Builder.RealFloat.Internal
107107
Data.ByteString.Builder.RealFloat.TableGenerator
108108
Data.ByteString.Internal.Type
109-
Data.ByteString.Lazy.Internal.Deque
110109
Data.ByteString.Lazy.ReadInt
111110
Data.ByteString.Lazy.ReadNat
112111
Data.ByteString.ReadInt

tests/Properties/ByteString.hs

Lines changed: 26 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,10 @@ import Test.Tasty
8080
import Test.Tasty.QuickCheck
8181
import QuickCheckUtils
8282

83+
#ifdef BYTESTRING_LAZY
84+
import Data.Int
85+
#endif
86+
8387
#ifndef BYTESTRING_CHAR8
8488
toElem :: Word8 -> Word8
8589
toElem = id
@@ -114,16 +118,25 @@ instance Arbitrary Natural where
114118

115119
testRdInt :: forall a. (Arbitrary a, RdInt a) => String -> TestTree
116120
testRdInt s = testGroup s $
117-
[ testProperty "from string" $ \ prefix value suffix ->
121+
[ testProperty "from string" $ int64OK $ \value prefix suffix ->
118122
let si = show @a value
119123
b = prefix <> B.pack si <> suffix
120124
in fmap (second B.unpack) (bread @a b)
121125
=== sread @a (B.unpack prefix ++ si ++ B.unpack suffix)
122-
, testProperty "from number" $ \n ->
126+
, testProperty "from number" $ int64OK $ \n ->
123127
bread @a (B.pack (show n)) === Just (n, B.empty)
124128
]
125129
#endif
126130

131+
intToIndexTy :: Int -> IndexTy
132+
#ifdef BYTESTRING_LAZY
133+
type IndexTy = Int64
134+
intToIndexTy = fromIntegral @Int @Int64
135+
#else
136+
type IndexTy = Int
137+
intToIndexTy = id
138+
#endif
139+
127140
tests :: [TestTree]
128141
tests =
129142
[ testProperty "pack . unpack" $
@@ -308,7 +321,7 @@ tests =
308321
#endif
309322

310323
, testProperty "drop" $
311-
\n x -> B.unpack (B.drop n x) === List.genericDrop n (B.unpack x)
324+
\(intToIndexTy -> n) x -> B.unpack (B.drop n x) === List.genericDrop n (B.unpack x)
312325
, testProperty "drop 10" $
313326
\x -> let n = 10 in B.unpack (B.drop n x) === List.genericDrop n (B.unpack x)
314327
, testProperty "drop 2^31" $
@@ -325,7 +338,7 @@ tests =
325338
#endif
326339

327340
, testProperty "take" $
328-
\n x -> B.unpack (B.take n x) === List.genericTake n (B.unpack x)
341+
\(intToIndexTy -> n) x -> B.unpack (B.take n x) === List.genericTake n (B.unpack x)
329342
, testProperty "take 10" $
330343
\x -> let n = 10 in B.unpack (B.take n x) === List.genericTake n (B.unpack x)
331344
, testProperty "take 2^31" $
@@ -342,11 +355,11 @@ tests =
342355
#endif
343356

344357
, testProperty "dropEnd" $
345-
\n x -> B.dropEnd n x === B.take (B.length x - n) x
358+
\(intToIndexTy -> n) x -> B.dropEnd n x === B.take (B.length x - n) x
346359
, testProperty "dropWhileEnd" $
347360
\f x -> B.dropWhileEnd f x === B.reverse (B.dropWhile f (B.reverse x))
348361
, testProperty "takeEnd" $
349-
\n x -> B.takeEnd n x === B.drop (B.length x - n) x
362+
\(intToIndexTy -> n) x -> B.takeEnd n x === B.drop (B.length x - n) x
350363
, testProperty "takeWhileEnd" $
351364
\f x -> B.takeWhileEnd f x === B.reverse (B.takeWhile f (B.reverse x))
352365

@@ -366,7 +379,7 @@ tests =
366379
, testProperty "compareLength 4" $
367380
\x (toElem -> c) -> B.compareLength (B.snoc x c <> undefined) (B.length x) === GT
368381
, testProperty "compareLength 5" $
369-
\x n -> B.compareLength x n === compare (B.length x) n
382+
\x (intToIndexTy -> n) -> B.compareLength x n === compare (B.length x) n
370383
, testProperty "dropEnd lazy" $
371384
\(toElem -> c) -> B.take 1 (B.dropEnd 1 (B.singleton c <> B.singleton c <> B.singleton c <> undefined)) === B.singleton c
372385
, testProperty "dropWhileEnd lazy" $
@@ -470,7 +483,8 @@ tests =
470483
(l1 == l2 || l1 == l2 + 1) && sum (map B.length splits) + l2 == B.length x
471484

472485
, testProperty "splitAt" $
473-
\n x -> (B.unpack *** B.unpack) (B.splitAt n x) === List.genericSplitAt n (B.unpack x)
486+
\(intToIndexTy -> n) x -> (B.unpack *** B.unpack) (B.splitAt n x)
487+
=== List.genericSplitAt n (B.unpack x)
474488
, testProperty "splitAt 10" $
475489
\x -> let n = 10 in (B.unpack *** B.unpack) (B.splitAt n x) === List.genericSplitAt n (B.unpack x)
476490
, testProperty "splitAt (2^31)" $
@@ -594,13 +608,13 @@ tests =
594608
#endif
595609

596610
, testProperty "index" $
597-
\(NonNegative n) x -> fromIntegral n < B.length x ==> B.index x (fromIntegral n) === B.unpack x !! n
611+
\(NonNegative n) x -> intToIndexTy n < B.length x ==> B.index x (intToIndexTy n) === B.unpack x !! n
598612
, testProperty "indexMaybe" $
599-
\(NonNegative n) x -> fromIntegral n < B.length x ==> B.indexMaybe x (fromIntegral n) === Just (B.unpack x !! n)
613+
\(NonNegative n) x -> intToIndexTy n < B.length x ==> B.indexMaybe x (intToIndexTy n) === Just (B.unpack x !! n)
600614
, testProperty "indexMaybe Nothing" $
601-
\n x -> (n :: Int) < 0 || fromIntegral n >= B.length x ==> B.indexMaybe x (fromIntegral n) === Nothing
615+
\n x -> n < 0 || intToIndexTy n >= B.length x ==> B.indexMaybe x (intToIndexTy n) === Nothing
602616
, testProperty "!?" $
603-
\n x -> B.indexMaybe x (fromIntegral (n :: Int)) === x B.!? (fromIntegral n)
617+
\(intToIndexTy -> n) x -> B.indexMaybe x n === x B.!? n
604618

605619
#ifdef BYTESTRING_CHAR8
606620
, testProperty "isString" $

0 commit comments

Comments
 (0)