[GHC] #3116: missed optimisation opportunity with lazy ByteStrings
GHC
trac at galois.com
Mon Mar 23 07:38:26 EDT 2009
#3116: missed optimisation opportunity with lazy ByteStrings
-----------------------------+----------------------------------------------
Reporter: duncan | Owner:
Type: bug | Status: new
Priority: normal | Component: Compiler
Version: 6.10.1 | Severity: normal
Keywords: | Testcase:
Os: Unknown/Multiple | Architecture: Unknown/Multiple
-----------------------------+----------------------------------------------
With strict `ByteString` an a simple tail recursive iteration pattern we
get perfect code.
First the definitions:
{{{
data ByteString
= BS {-# UNPACK #-} !(ForeignPtr Word8) -- payload
{-# UNPACK #-} !Int -- offset
{-# UNPACK #-} !Int -- length
null :: ByteString -> Bool
null (BS _ _ l) = l <= 0
tail :: ByteString -> ByteString
tail (BS p s l)
| l <= 0 = error "ByteString.tail: empty ByteString"
| otherwise = BS p (s+1) (l-1)
}}}
Now a trivial iteration pattern:
{{{
length :: ByteString -> Int
length = go 0
where
go !n bs | null bs = n
| otherwise = go (n+1) (tail bs)
}}}
Perfect core code (edited for clarity):
{{{
go :: Int# -> Addr# -> ForeignPtrContents
-> Int# -> Int# -> Int#
go = \n p fpc o l ->
case l <=# 0 of
False -> go (n +# 1) p fpc (o +# 1) (l -# 1)
True -> n
length :: ByteString -> GHC.Base.Int
length = \bs ->
case bs of
BS p fpc 0 l ->
case go 0 p fpc 0 l of
n -> I# n
}}}
This worked because strict `ByteString` is a single constructor data type
which allows it to be unpacked into separate parameters in the recursive
call.
Now, lets try the same with lazy `ByteStrings`:
{{{
data ByteString
= Empty
| Chunk {-# UNPACK #-} !StrictBS.ByteString ByteString
}}}
This of course has two constructors. It's built for call-pattern
specialisation.
Now the ops:
{{{
null :: ByteString -> Bool
null Empty = True
null _ = False
tail :: ByteString -> ByteString
tail Empty = error "empty tail"
tail (Chunk (S.BS fp s 1) cs) = cs
tail (Chunk (S.BS fp s l) cs) = Chunk (S.BS fp (s+1) (l-1)) cs
}}}
We can use the exact same code for tail, but now for the lazy `ByteString`
type:
{{{
length :: ByteString -> Int
length = go 0
where
go !n bs | null bs = n
| otherwise = go (n+1) (tail bs)
}}}
But, oh noes!! The optimisation does not work:
{{{
go :: Int# -> ByteString -> Int#
go = \n bs ->
case bs of
Empty -> n
Chunk p fpc o l bs' ->
go (n +# 1)
(case l of
1 -> bs'
_ -> Chunk p fpc (o +# 1) (l -# 1) bs')
length :: ByteString -> Int
length = \bs ->
case go 0 bs of
n -> I# n
}}}
However this is not because call pattern specialisation didn't do what we
wanted. We know this for several reasons. One, if we remove the case
{{{
tail (Chunk (S.BS fp s 1) cs) = cs
}}}
then call pattern specialisation works and the code ends up as a perfect
loop. Of course we need that case for correctness.
Also, if we change the definition of lazy `ByteString` to be a single
constructor and represent the end by an empty chunk then we still get
essentially the same code.
Also, if we use `uncons` instead of `null` and `tail` then we effectively
perform by hand the missing optimisation transformation and get perfect
code (and call-pattern specialisation happens exactly as expected).
{{{
length :: ByteString -> Int
length = go 0
where
go !n bs = case uncons bs of
Nothing -> n
Just (_, bs') -> go (n+1) bs'
uncons :: ByteString -> Maybe (Word8, ByteString)
uncons Empty = Nothing
uncons (Chunk c cs)
| StrictBS.length c == 1
= Just (S.unsafeHead c, cs)
| otherwise
= Just (S.unsafeHead c, Chunk (S.unsafeTail c) cs)
}}}
This version with `uncons` gives us perfect code:
{{{
go_chunk :: ByteString -> Int# -> Int#
-> ForeignPtrContents -> Addr# -> Int# -> Int#
go_chunk = \bs' l o fpc p n ->
case l of
1 -> go (n +# 1) bs'
_ -> go_chunk bs' (l -# 1) (o +# 1) fpc p (n +# 1)
go :: Int# -> ByteString -> Int#
go = \n bs ->
case bs of
Empty -> n
Chunk p fpc o l bs' ->
case l of
1 -> go (n +# 1) bs'
_ -> go_chunk bs' (l -# 1) (o +# 1) fpc p (n +# 1)
length :: ByteString -> Int
length = \bs ->
case go 0 bs of
n -> I# n
}}}
and we can see the specialisation rule that ghc invented for us:
{{{
forall bs l o fpc p n.
go n (Chunk p fpc o l bs)
= go_chunk bs l o fpc p n
}}}
Aside: looks like there's an extra/missing reverse in there somewhere.
The problem with the `head` / `tail` version is that the following
transformation is never performed and so the opportunity for call pattern
specialisation (or even simple worker/wrapper unpacking) is never exposed:
{{{
go (n+1)
(case l of
1 -> bs'
_ -> Chunk p fpc (o+1) (l-1) bs')
=
case l of
1 -> go (n+1) bs'
_ -> go (n+1) (Chunk p fpc (o+1) (l-1) bs')
}}}
which is the fragment inside the inlined definition of the go worker
function:
{{{
go !n bs = case bs of
Empty -> n
Chunk p fpc o l bs' ->
go (n+1)
(case l of
1 -> bs'
_ -> Chunk p fpc (o+1) (l-1) bs')
}}}
So `go` is tail recursive and strict in both arguments. This situation
will arise whenever we have something like
{{{
go (tail xs)
}}}
and `tail` involves a case analysis. Since `go` is just a function call
there is no problem in duplicating it into the two branches of `tail`. The
fact that it enables the call-pattern specialisation makes this a huge
win.
This will happen with lazy `ByteString`s or any other chunked
representation using trees. We really need the per-chunk inner loop to be
good with just a single test in the fast path to see if we're at the end
of the chunk. When we get to the end of the chunk we can move into the
slow path and do more cunning things with chunks and trees and lazyness.
But the overall speed of these kinds of operations is determined by the
quality of the inner loop. If that inner loop has to allocate a fresh
constructor each time round then we lose.
In principle, if we can optimise perfectly then there is no reason for
strict `ByteString` to have faster code than lazy `ByteString`, because
their inner loops should look exactly the same.
--
Ticket URL: <http://hackage.haskell.org/trac/ghc/ticket/3116>
GHC <http://www.haskell.org/ghc/>
The Glasgow Haskell Compiler
More information about the Glasgow-haskell-bugs
mailing list