Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Performant Polymorphism: Rewrite Rules in Haskell

Performant Polymorphism: Rewrite Rules in Haskell

GHC usually does an excellent job of transforming well written
Haskell code into efficient machine code, but sometimes "fast" is
not "fast enough". Common optimisation techniques when dealing with
concrete data types often do not apply to polymorphic data and
functions. A concise, generic algorithm may perform poorly for some
types, but providing a faster version with a less polymorphic type
sacrifices reusability and parametricity! What's a principled
programmer to do?

Fortunately GHC has got your back here, too. In this talk we will
learn about GHC's *rewrite rules* feature, which can be used for
substituting alternative, better performing implementations of
polymorphic functions at particular (less polymorphic) types,
without changing the type signature that users see, preserving reuse
and parametricity. We will see also how to define transformation
rules that employ theorems (free or otherwise) to optimise programs.

We will also briefly examine how rules are applied by observing the
firing of rules and changes effected in the produced Core (GHC's
fully desugared intermediate language), and see how to control the
order of inlining and rewrite rules to achieve the desired
outcome.

Finally, we'll look at a real-world example of how rewrite rules are
used in the 'fresnel'[1] library, a unified parser-printer
combinator library based on the 'Cons' abstraction from the 'lens'
library, to dramatically speed up printing for certain output types.

This will be a hands-on talk with live coding, benchmarking and
profiling (no optimising without metrics!) and Core spelunking.
Audience members familiar with Haskell should expect to learn some
basics of Haskell benchmarking and profiling, gain an understanding
of when and how to use rewrite rules in their own code, and
walk out feeling comfortable (or less trepidatious, perhaps) about
reading and analysing their programs' Core.

[1] https://github.com/frasertweedale/hs-fresnel

Fraser Tweedale

May 08, 2017
Tweet

More Decks by Fraser Tweedale

Other Decks in Programming

Transcript

  1. There’s a hole in my program. . . _ ::

    B.ByteString -> L.ByteString _ :: B.ByteString -> [Word8] _ :: T.Text -> String _ :: String -> T.Text _ :: TL.Text -> T.Text
  2. There’s a hole in my program. . . L.fromStrict ::

    B.ByteString -> L.ByteString B.unpack :: B.ByteString -> [Word8] T.unpack :: T.Text -> String T.pack :: String -> T.Text TL.toStrict :: TL.Text -> T.Text
  3. Control.Lens.Cons class Cons s t a b where _Cons ::

    Prism s t (a, s) (b, t) instance ByteString ByteString Word8 Word8 instance Text Text Char Char instance [a] [b] a b cons :: Cons s s a a => a -> s -> s uncons :: Cons s s a a => s -> Maybe (a, s)
  4. recons recons :: (Cons s1 s1 a a, Cons s2

    s2 a a, AsEmpty s2) => s1 -> s2
  5. There’s a hole in my program. . . _ ::

    B.ByteString -> L.ByteString _ :: B.ByteString -> [Word8] _ :: T.Text -> String _ :: String -> T.Text _ :: TL.Text -> T.Text
  6. There’s a hole in my program. . . recons ::

    B.ByteString -> L.ByteString recons :: B.ByteString -> [Word8] recons :: T.Text -> String recons :: String -> T.Text recons :: TL.Text -> T.Text
  7. Parametricity mibbup :: (Cons s1 s1 a a, Cons s2

    s2 a a, AsEmpty s2) => s1 -> s2 wossit :: [Word8] -> L.ByteString
  8. Parametricity recons :: (Cons s1 s1 a a, Cons s2

    s2 a a, AsEmpty s2) => s1 -> s2 L.pack :: [Word8] -> L.ByteString
  9. criterion whnf :: (a -> b) -> a -> Benchmarkable

    nf :: NFData b => (a -> b) -> a -> Benchmarkable bench :: String -> Benchmarkable -> Benchmark
  10. Glasgow Haskell Compiler Haskell desugars to Core Simplifier applies Core-to-Core

    transformations Core → STG → C-- Compile C-- to machine code Secrets of the Haskell Inliner: https://www.microsoft.com/en-us/research/wp-content/uploads/2016/07/inline-jfp.pdf Compilation by transformation: https://www.microsoft.com/en-us/research/wp-content/uploads/1998/09/comp-by-trans-scp.pdf
  11. Phase control -- Before phase 2 Phase 2 and later

    {-# INLINE f #-} -- Yes Yes {-# NOINLINE f #-} -- No No {-# INLINE [2] f #-} -- No Yes {-# INLINE [~2] f #-} -- Yes No {-# NOINLINE [2] f #-} -- No Maybe {-# NOINLINE [~2] f #-} -- Maybe No
  12. Rewrite rules {-# RULES "map/map" forall f g xs. map

    f (map g xs) = map (f . g) xs #-}
  13. Rewrite rules {-# RULES "map/map" [2] forall f g xs.

    map f (map g xs) = map (f . g) xs #-}
  14. Rewrite rules LHS rewrites to RHS always exported compile with

    -O no termination / semantic equivalence checks
  15. Rewrite rules - fusion data Stream a where Stream ::

    (s -> Step s a) -> s -> Stream a data Step s a = Yield a s | Skip s | Done map :: (a -> b) -> [a] -> [b] map f = unstream . map . stream -- http://code.haskell.org/~dons/papers/icfp088-coutts.pdf
  16. Rewrite rules - fusion map g . map f =

    unstream . map g . stream . unstream . map f . stream
  17. Rewrite rules - fusion map g . map f =

    unstream . map g . id . map f . stream
  18. Rewrite rules - fusion map g . map f =

    unstream . map g . map f . stream
  19. concise Control.Lens.Cons.Extras.recons :: (Cons s1 s1 a a, Cons s2

    s2 a a, AsEmpty s2) => Getter s1 s2 -- https://hackage.haskell.org/package/concise
  20. fresnel sepBy :: Grammar s a -> Grammar s ()

    -> Grammar s [a] sepByT :: Grammar T.Text a -> Grammar T.Text () -> Grammar T.Text [a] sepByT g sep = prism (\(as, s) -> T.intercalate (print sep ()) (print g <$> as) <> s) (preview (sepBy g sep)) {-# RULES "sepBy/T" sepBy = sepByT #-} -- https://github.com/frasertweedale/hs-fresnel
  21. Recap The three Rs: reuse, refactoring, readability Rewrite rules: make

    generic functions go fast Other optimisations enabled by rewrite rules Phase control: make inliner and simplifier play nice