[GHC] #10565: GHC 7.10.2 RC: the impossible happened on hPDB-examples-1.2.0.2
GHC
ghc-devs at haskell.org
Wed Jul 1 10:21:06 UTC 2015
#10565: GHC 7.10.2 RC: the impossible happened on hPDB-examples-1.2.0.2
---------------------------------+-----------------------------------------
Reporter: snoyberg | Owner: bgamari
Type: bug | Status: new
Priority: normal | Milestone: 7.10.2
Component: Compiler | Version: 7.10.2-rc1
Resolution: | Keywords:
Operating System: Linux | Architecture: x86_64 (amd64)
Type of failure: None/Unknown | Test Case:
Blocked By: | Blocking:
Related Tickets: | Differential Revisions:
---------------------------------+-----------------------------------------
Changes (by bgamari):
* owner: => bgamari
Comment:
Thankfully this doesn't appear to be related to #10527 (which is quite a
hard nut to crack). Instead this is due to the fact that `guessElement` is
marked `INLINE`. To see why this is problematic, we have to look at how
this function is desugared. The pattern match is using
`OverloadedStrings`, which desugars this,
{{{#!hs
f :: ByteString -> String
f "foo" = 1
f "bar" = 2
f _ = 3
}}}
Roughly into this,
{{{#!hs
f :: ByteString -> String
f x | x == (fromString "foo" :: ByteString) = 1
f x | x == (fromString "bar" :: ByteString) = 2
f _ = 3
}}}
Note that we are using using `ByteString`'s `Eq` instance here to check
the pattern match. These guards are then desugared to nested `case`
analyses in the desugared Core.
It is with this Core that we enter the Core-to-Core optimization pipeline.
`ByteString`'s `Eq` instance looks like this,
{{{#!hs
instance Eq ByteString where
(==) = eq
eq :: ByteString -> ByteString -> Bool
eq a@(PS fp off len) b@(PS fp' off' len')
| len /= len' = False -- short cut on length
| fp == fp' && off == off' = True -- short cut for the same string
| otherwise = compareBytes a b == EQ
{-# INLINE eq #-}
compareBytes :: ByteString -> ByteString -> Ordering
compareBytes = ...
}}}
Despite `(==)` not having an explicit `INLINE` pragma, GHC is still quite
eager to inline in. Consequently our chain of equality comparisons in `f`
quickly explodes into a large ball of `IO`
To make matters a bit worse, the literal of each alternative is itself a
fair amount of code. e.g. the `"OG1"` literal is floated out into a top-
level binding looking like this,
{{{#!hs
lvl10_r5ix :: BS.ByteString
[GblId, Str=DmdType]
lvl10_r5ix =
case newMutVar# @ Finalizers @ RealWorld NoFinalizers realWorld#
of _ [Occ=Dead] { (# ipv_a3qk, ipv1_a3ql #) ->
let {
s_a263 :: Addr#
[LclId, Str=DmdType]
s_a263 = "OG1"# } in
case {__pkg_ccall bytestring-0.10.6.0 strlen Addr#
-> State# RealWorld -> (# State#
RealWorld, Word# #)}_a3rx
s_a263 ipv_a3qk
of _ [Occ=Dead] { (# ds3_a3rC, ds4_a3rD #) ->
Data.ByteString.Internal.PS
s_a263 (PlainForeignPtr ipv1_a3ql) 0# (word2Int# ds4_a3rD)
}
}
}}}
== Resolution ==
Above we saw that the original code has a few issues,
1. `ByteString`'s sizeable `(==)` implementation is inlined once for each
alternative, resulting in a large quantity of code for `f`
2. The pattern match desugars to a linear search where some more
efficient strategy would be desired
Let's first look at how we might fix (1), which causes the simplifier
blow-up noted in this bug. After this we can move on to improving the
asymptotic issue, (2).
**Note**: To some extent, the pattern matching behavior exposed by
`OverloadedStrings` is a bit dangerous as it emulates pattern matching,
something that most Haskellers regard as "cheap" (up to the cost of
forcing the scrutinee), with a call to an arbitrarily complex function.
Issue (1) arises from the desugaring of the `OverloadedStrings` pattern
match, which produces a new test for every alternative. GHC will then
happily inline `(==)` in to each of these despite the fact that there is
no benefit to doing so. Ideally we would want to make it obvious to GHC
that the comparison should be shared. One way to accomplish this would be
to encode the alternatives as an associated list and use `lookup`,
{{{#!hs
guessElement :: BS.ByteString -> String
guessElement = \e -> fromMaybe "" $ lookup e els
where
els :: [(ByteString, String)]
els =
[ ("C" , "C"),
("C1'" , "C"),
("C2" , "C"),
...
]
}}}
Here we ensure that there is exactly one use of `(==)`, preventing the
explosion of code.
While we are looking at optimizations we might also consider that
`ByteString` is generally best used with large strings. In the case of
small comparisons like this it might be worthwhile to avoid using
`ByteString`'s `Eq` altogether and instead compare `String`s,
{{{#!hs
guessElement :: BS.ByteString -> String
guessElement = \e -> fromMaybe "" $ lookup (BS.unpack e) els
where
els :: [(String, String)]
els = [ ... ]
}}}
However, both of these avoid solving the issue of linear search. For this
we can turn to any number of finite map data structures. For instance, we
know that `ByteString` has an `Ord` instance so we can employ `Data.Map`
from `containers`,
{{{#!hs
guessElement :: BS.ByteString -> String
guessElement = \e -> fromMaybe "" $ M.lookup e els
where
els :: M.Map BS.ByteString String
els = M.fromList [ ... ]
}}}
Of course, it also has a `Hashable` instance so we can also try
`Data.HashMap` from `unordered-containers`. I've prepared a small
benchmark
(https://github.com/bgamari/ghc-T10565) comparing these options. The
results from a run on my Core i5 laptop can be found below.
||= **Implementation** =||||= **Time per iteration (μs)** =||
|| Original pattern match || 21.2 || ± 1.9 ||
|||||| ''Association list'' ||
|| `lookup` on `ByteString` || 42.1 || ± 2.0 ||
|| `lookup` on `String` || 38.9 || ± 1.8 ||
|||||| ''Ordered map'' ||
|| `lookup` on `ByteString` || 6.6 || ± 0.5 ||
|| `lookup` on `String` || 8.5 || ± 0.4 ||
|||||| ''Unordered map'' ||
|| `lookup` on `ByteString` || 4.1 || ± 0.1 ||
|| `lookup` on `String` || 4.3 || ± 0.5 ||
== Improving GHC? ==
In an ideal world GHC would be able to look at the original code and
independently determine something like we determined above. The key
insight that we had here is that our `ByteString` type has structure
beyond the `Eq` used by `OverloadedString`'s pattern matching behavior.
Can we teach GHC to have this same insight?
It would be possible for the compiler to construct a similar lookup data
structure for pattern matches on `IsString` types also having an `Ord`
instance. The trouble with this is that the operational behavior of the
program is now dependent upon which instances are in scope. This seems
like a very undesirable property.
One could instead add a `compare' :: a -> a -> Ordering` to `IsString`,
allowing GHC to generate efficient logarithmic lookups without a
dependence on which instances are available.
The above option, however, prevents us from using the best tool in our
arsenault, the `HashMap`. Another more general possibility would be to add
function to `IsString` to explicitly handle matching against groups of
patterns. The compiler would collect the pattern matches which can be
simultaneously resolved and pass them, along with the scrutinee, to a
`match` function. This might look like this,
{{{#!hs
class IsString a where
fromString :: String -> a
toString :: a -> String
match :: [(a, b)] -> a -> b
}}}
This, however, carries with it a number of issues. Perhaps most concerning
is the fact that now a library author has the ability to break pattern
matching semantics (consider, for instance, `match alts _ = last alts`).
Moreover, all of these have unfortunate compatibility issues.
Finally, these options all interact very poorly with more complex pattern
matches. Take for instance,
{{{#!hs
f :: String -> Int -> Result
f "a" _ = resultA
f "b" _ = resultB
f _ 3 = resultC
f "c" _ = resultD
f "d" 4 = resultE
f "d" _ = resultF
f _ _ = resultG
}}}
It would be rather difficult to define matching semantics which took
advantage of the above `match` function yet treated cases like the above
(or even simpler cases). Consequently, in many cases the user would still
need to fall back to the current approach of manually implementing their
desired match behavior.
Ultimately, these are decisions that should arguably be left to the user
anyways. Data structure choice will be highly specific to the structure of
the alternatives and values being scrutinized.
--
Ticket URL: <http://ghc.haskell.org/trac/ghc/ticket/10565#comment:2>
GHC <http://www.haskell.org/ghc/>
The Glasgow Haskell Compiler
More information about the ghc-tickets
mailing list