packages feed

loopbreaker (empty) → 0.1.1.0

raw patch · 13 files changed

+530/−0 lines, 13 filesdep +basedep +containersdep +ghcsetup-changed

Dependencies added: base, containers, ghc, hspec, inspection-testing, loopbreaker, syb

Files

+ ChangeLog.md view
@@ -0,0 +1,10 @@+# Changelog for Loopbreaker++## 0.1.1.0+- follow `INLINE` pragmas when adding loopbreakers+- support local declarations (`let` and `where` bindings)++## 0.1.0.0+- initial release++## Unreleased changes
+ LICENSE view
@@ -0,0 +1,30 @@+Copyright Matej Nižník, Sandy Maguire (c) 2019++All rights reserved.++Redistribution and use in source and binary forms, with or without+modification, are permitted provided that the following conditions are met:++    * Redistributions of source code must retain the above copyright+      notice, this list of conditions and the following disclaimer.++    * Redistributions in binary form must reproduce the above+      copyright notice, this list of conditions and the following+      disclaimer in the documentation and/or other materials provided+      with the distribution.++    * Neither the name of Matej Nižník nor the names of other+      contributors may be used to endorse or promote products derived+      from this software without specific prior written permission.++THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ README.md view
@@ -0,0 +1,44 @@+# loopbreaker++Performance of libraries like [polysemy](https://github.com/polysemy-research/polysemy)+depends on code being aggresively inlined. Problem is that GHC is not very+keen on inlining self-recursive definitions. Luckily, there's a way we can+trick compiler to do so: by introducing intermediate _loopbreaker_ that simply+calls our original function:+```+fact :: Int -> Int+fact 0 = 1+fact n = n * fact' (n - 1)+{-# INLINE fact #-}++fact' :: Int -> Int+fact' = fact+{-# NOINLINE fact' #-}+```+But because this is ugly boilerplate nobody wants to write, we created GHC+plugin that searches for such recursive definitions and automatically inserts+loopbreakers during compilation.++To quote [isovector](https://github.com/isovector):+> As described in [Writing Custom Optimization Passes](https://reasonablypolymorphic.com/blog/writing-custom-optimizations/),+The `polysemy-plugin` has had support for creating explicit loopbreakers for+self-recursive functions. The result is pretty dramatic code improvements in a+lot of cases when `-O2` is turned on.++> Rather embarrassingly, after publishing that post, it turned out that my+implementation didn't in fact improve optimizations. Sure, it spit out the+correct code, but it was being done too late, and some special analysis passes+had already run. The result: we'd generate loopbreakers, but they wouldn't be+used.++> [TheMatten](https://github.com/TheMatten) took it upon himself to fix this.+There's no trick --- just do the same transformations after renaming, rather+than after typechecking. We realized this plugin is useful outside of+polysemy, so it's been released as a standalone package.++## Usage++Add `-fplugin=Loopbreaker` into your `package.yaml` or specific module. This+will cause the plugin to generate explicit loopbreakers for any self-recursive+functions marked as `INLINE`, which can dramatically improve their+performance. See documentation for more info.
+ Setup.hs view
@@ -0,0 +1,2 @@+import Distribution.Simple+main = defaultMain
+ loopbreaker.cabal view
@@ -0,0 +1,71 @@+cabal-version: 1.12+name: loopbreaker+version: 0.1.1.0+license: BSD3+license-file: LICENSE+copyright: 2019 Matej Nižník,+           2019 Sandy Maguire+maintainer: matten@tuta.io+author: Matej Nižník,+        Sandy Maguire+homepage: https://github.com/polysemy-research/loopbreaker#readme+bug-reports: https://github.com/polysemy-research/loopbreaker/issues+synopsis: inline self-recursive definitions+description:+    Please see the README file on Github for more info+category: Plugin+build-type: Simple+extra-source-files:+    README.md+    ChangeLog.md++source-repository head+    type: git+    location: https://github.com/polysemy-research/loopbreaker++library+    exposed-modules:+        Loopbreaker+    hs-source-dirs: src+    other-modules:+        Loopbreaker.InlineRecCalls+        Loopbreaker.Utils+        Paths_loopbreaker+    default-language: Haskell2010+    default-extensions: ConstraintKinds FlexibleContexts KindSignatures+                        LambdaCase MultiWayIf NamedFieldPuns OverloadedStrings+                        ScopedTypeVariables TupleSections TypeFamilies UnicodeSyntax+                        ViewPatterns+    ghc-options: -Wall+    build-depends:+        base >=4.7 && <5,+        containers ==0.6.*,+        ghc ==8.6.*,+        syb ==0.7.*++test-suite loopbreaker-test+    type: exitcode-stdio-1.0+    main-is: Spec.hs+    build-tool-depends: hspec-discover:hspec-discover -any+    hs-source-dirs: test+    other-modules:+        DisableFlagSpec+        InlineRecCallsSpec+        PragmaDetectionSpec+        TestUtils+        Paths_loopbreaker+    default-language: Haskell2010+    default-extensions: ConstraintKinds FlexibleContexts KindSignatures+                        LambdaCase MultiWayIf NamedFieldPuns OverloadedStrings+                        ScopedTypeVariables TupleSections TypeFamilies UnicodeSyntax+                        ViewPatterns+    ghc-options: -Wall -threaded -rtsopts -with-rtsopts=-N+                 -fplugin=Loopbreaker+    build-depends:+        base >=4.7 && <5,+        containers ==0.6.*,+        ghc ==8.6.*,+        hspec >=2.6.1 && <3,+        inspection-testing >=0.4.2.1 && <0.5,+        loopbreaker -any,+        syb ==0.7.*
+ src/Loopbreaker.hs view
@@ -0,0 +1,54 @@+-- | To use plugin, you can:+--+-- * Either enable the plugin globally in your project configuration's+-- @ghc-options@ field (@package.yaml@ or @\<name\>.cabal@):+--+-- @+-- # package.yaml+-- ...+--+-- ghc-options:+-- - -fplugin=Loopbreaker+--+-- ...+-- @+--+-- * Or alternatively, just enable the plugin in specific modules that may+-- benefit from it's use:+--+-- @+-- -- \<name\>.hs+--+-- {-\# OPTIONS_GHC -fplugin=Loopbreaker \#-}+--+-- ...+-- @+--+-- If you decide to enable it globally, you can selectively disable it in+-- specific modules using @disable@ option:+--+-- @+-- {-\# OPTIONS_GHC -fplugin-opt=Loopbreaker:disable \#-}+--+-- ...+-- @+--+-- Now, in modules where the plugin is enabled, any self-recursive functions+-- marked as @INLINE@ may have their performance greatly improved.++-- TODO(Matej): update docs when loopbreakers for local definitions get added++module Loopbreaker (plugin) where++import GhcPlugins++import Loopbreaker.Utils+import Loopbreaker.InlineRecCalls (action)+++------------------------------------------------------------------------------+plugin :: Plugin+plugin = defaultPlugin+  { pluginRecompile     = purePlugin+  , renamedResultAction = \opts -> traverse (action opts) .: (,)+  }
+ src/Loopbreaker/InlineRecCalls.hs view
@@ -0,0 +1,161 @@+{-# LANGUAGE TemplateHaskell #-}++module Loopbreaker.InlineRecCalls (action) where++import           Control.Arrow hiding ((<+>))+import           Data.Generics+import           Data.Kind+import           Data.Map (Map)+import qualified Data.Map as M+import           Data.Maybe+import           Data.Set (Set)+import qualified Data.Set as S++import Bag+import ErrUtils+import GhcPlugins hiding ((<>), debugTraceMsg)+import HsSyn+import MonadUtils+++------------------------------------------------------------------------------+type MonadInline m = ((MonadUnique m, MonadIO m, HasDynFlags m) :: Constraint)++------------------------------------------------------------------------------+-- | Forces compiler to inline functions by creating loopbreaker with NOINLINE+-- pragma, changing recursive calls to use it and by adding INLINE pragma to+-- the original function.+action :: MonadInline m+       => [CommandLineOption] -> HsGroup GhcRn -> m (HsGroup GhcRn)+action opts group@HsGroup{ hs_valds } = do+  let shouldDisable = "disable" `elem` opts++  dyn_flags <- getDynFlags++  if not shouldDisable && optLevel dyn_flags > 0+    then do+      liftIO $ showPass dyn_flags "Break loops"+      valds' <- inlineRecCalls hs_valds+      pure group{ hs_valds = valds' }+    else+      pure group++action _ _ = error "Loopbreaker.InlineRecCalls.action: expected renamed group"++------------------------------------------------------------------------------+inlineRecCalls :: MonadInline m => HsValBinds GhcRn -> m (HsValBinds GhcRn)+inlineRecCalls (XValBindsLR (NValBinds binds sigs)) = do+  let (types, inlined) = typesFromSigs &&& inlinedFromSigs $ unLoc <$> sigs++  (binds', extra_sigs) <- second concat . unzip+                      <$> traverse (inlineRecCall types inlined) binds++  pure $ XValBindsLR $ NValBinds binds' $ sigs ++ extra_sigs++-- TODO: should we throw an error here instead?+inlineRecCalls val_binds = pure val_binds++------------------------------------------------------------------------------+typesFromSigs :: Ord (IdP p) => [Sig p] -> Map (IdP p) (LHsSigWcType p)+typesFromSigs = M.fromList . concatMap sigToTups where+  sigToTups (TypeSig _ names type_) = (,type_) . unLoc <$> names+  sigToTups _                       = []++------------------------------------------------------------------------------+inlinedFromSigs :: Ord (IdP p) => [Sig p] -> Set (IdP p)+inlinedFromSigs = S.fromList . catMaybes . map nameIfInlineSig where+  nameIfInlineSig (InlineSig _ (L _ name) pragma)+    | isInlinePragma pragma = Just name+  nameIfInlineSig _ = Nothing++------------------------------------------------------------------------------+-- | Inserts loopbreaker to recursive binding group of single binding and+-- emits necessary signatures.+inlineRecCall+  :: MonadInline m+  => Map Name (LHsSigWcType GhcRn)  -- ^ types of bindings+  -> Set Name                       -- ^ 'Loopbreaker' annotations+  -> (RecFlag, LHsBinds GhcRn)      -- ^ binding being inlined+  -> m ((RecFlag, LHsBinds GhcRn), [LSig GhcRn])+inlineRecCall types inlined (Recursive, binds)+  | (bagToList -> [L fun_loc fun_bind])           <- binds+  , FunBind{ fun_id = L _ fun_name, fun_matches } <- fun_bind+  , S.member fun_name inlined+  = do+  dyn_flags <- getDynFlags+  liftIO $ debugTraceMsg dyn_flags 2 $ text "Loopbreaker:" <+> ppr fun_name++  (loopb_name, loopb_decl) <- loopbreaker fun_name++  fun_matches' <- everywhereM ( fmap (replaceVarNamesT fun_name loopb_name)+                              . inlineLocalRecCallsM+                              ) fun_matches++  let m_loopb_sig = loopbreakerSig loopb_name <$> M.lookup fun_name types++  pure+    ( ( Recursive+      , listToBag+          [ L fun_loc fun_bind{ fun_matches = fun_matches' }+          , loopb_decl+          ]+      )+      -- If the original function didn't have type signature specified, we+      -- shouldn't have to have either+    , inlineSig noInlinePragma loopb_name : maybeToList m_loopb_sig+    )+-- We ignore mutually recursive and other bindings+inlineRecCall _ _ binds = pure (binds, [])++------------------------------------------------------------------------------+-- | Creates loopbreaker and it's name from name of the original function.+loopbreaker :: MonadUnique m => Name -> m (Name, LHsBind GhcRn)+loopbreaker fun_name =+  (id &&& loopbreakerDecl fun_name) <$> loopbreakerName fun_name++------------------------------------------------------------------------------+loopbreakerName :: MonadUnique m => Name -> m Name+loopbreakerName (occName -> occNameFS -> orig_fs) =+  flip mkSystemVarName (orig_fs <> "__Loopbreaker") <$> getUniqueM++------------------------------------------------------------------------------+loopbreakerDecl :: Name -> Name -> LHsBind GhcRn+loopbreakerDecl fun_name loopb_name =+  noLoc $ mkTopFunBind Generated (noLoc loopb_name)+    [ mkSimpleMatch (mkPrefixFunRhs $ noLoc loopb_name) [] $+        nlHsVar fun_name+    ]++------------------------------------------------------------------------------+-- | Creates loopbreaker type signature from type of original function.+loopbreakerSig :: Name -> LHsSigWcType GhcRn -> LSig GhcRn+loopbreakerSig loopb_name fun_type =+  noLoc $ TypeSig NoExt [noLoc loopb_name] fun_type++------------------------------------------------------------------------------+inlineSig :: (XInlineSig p ~ NoExt) => InlinePragma -> IdP p -> LSig p+inlineSig how name = noLoc $ InlineSig NoExt (noLoc name) how++------------------------------------------------------------------------------+-- | Contrary to 'neverInlinePragma', this has behaviour of 'NOINLINE' pragma.+noInlinePragma :: InlinePragma+noInlinePragma = defaultInlinePragma+  { inl_inline = NoInline+  , inl_act    = NeverActive+  }++------------------------------------------------------------------------------+-- | Transformation that applies loopbreakers to local bindings.+inlineLocalRecCallsM :: (MonadInline m, Typeable a) => a -> m a+inlineLocalRecCallsM = mkM $ \case+  (HsValBinds NoExt binds :: HsLocalBinds GhcRn)+    -> HsValBinds NoExt <$> inlineRecCalls binds+  e -> pure e++------------------------------------------------------------------------------+-- | Transformation that replaces every name in variable expression.+replaceVarNamesT :: Typeable a => Name -> Name -> a -> a+replaceVarNamesT from to = mkT $ \case+  HsVar NoExt (L loc name) :: HsExpr GhcRn+    | name == from -> HsVar NoExt $ L loc to+  e -> e
+ src/Loopbreaker/Utils.hs view
@@ -0,0 +1,7 @@+module Loopbreaker.Utils+  ( (.:)+  ) where+++(.:) :: (c -> d) -> (a -> b -> c) -> a -> b -> d+(.:) = (.).(.)
+ test/DisableFlagSpec.hs view
@@ -0,0 +1,34 @@+{-# OPTIONS_GHC -O2 -fplugin-opt=Loopbreaker:disable #-}++{-# LANGUAGE TemplateHaskell #-}++module DisableFlagSpec (spec) where++import TestUtils+++------------------------------------------------------------------------------+spec :: Spec+spec = describe "plugin" $ do+  it "should respect disable flag" $ do+    shouldFail $(inspectTest $ 'recursiveDefault === 'mutual)+    shouldFail $(inspectTest $ 'recursiveOn      === 'mutual)++------------------------------------------------------------------------------+recursiveDefault :: Int -> Int+recursiveDefault 0 = 1+recursiveDefault n = n * recursiveDefault (n - 1)++recursiveOn :: Int -> Int+recursiveOn 0 = 1+recursiveOn n = n * recursiveOn (n - 1)+{-# INLINE recursiveOn #-}++mutual :: Int -> Int+mutual 0 = 1+mutual n = n * mutual' (n - 1)+{-# INLINE mutual #-}++mutual' :: Int -> Int+mutual' = mutual+{-# NOINLINE mutual' #-}
+ test/InlineRecCallsSpec.hs view
@@ -0,0 +1,64 @@+{-# OPTIONS_GHC -O2 #-}++{-# LANGUAGE TemplateHaskell #-}++module InlineRecCallsSpec (spec) where++import TestUtils+++------------------------------------------------------------------------------+-- TODO: more tests+spec :: Spec+spec = describe "plugin" $ do+  it "should explicitly break recursion in global bindings" $ do+    shouldSucceed $(inspectTest $ 'recursive === 'mutual)+  -- TODO: implementation as Core pass to resolve this+  -- it "should work without signatures" $ do+  --   shouldSucceed $(inspectTest $ 'recursiveNoSig === 'mutual)+  it "should explicitly break recursion in where bindings" $ do+    shouldSucceed $(inspectTest $ 'localRecursiveWhere === 'localMutual)+  it "should explicitly break recursion in let bindings" $ do+    shouldSucceed $(inspectTest $ 'localRecursiveLet === 'localMutual)++------------------------------------------------------------------------------+recursive :: Int -> Int+recursive 0 = 1+recursive n = n * recursive (n - 1)+{-# INLINE recursive #-}++-- recursiveNoSig 0 = (1 :: Int)+-- recursiveNoSig n = n * recursiveNoSig (n - 1)+-- {-# INLINE recursiveNoSig #-}++mutual :: Int -> Int+mutual 0 = 1+mutual n = n * mutual' (n - 1)+{-# INLINE mutual #-}++mutual' :: Int -> Int+mutual' = mutual+{-# NOINLINE mutual' #-}++localRecursiveWhere :: Int -> Int+localRecursiveWhere = local where+  local 0 = 1+  local n = n * local (n - 1)+  {-# INLINE local #-}++localRecursiveLet :: Int -> Int+localRecursiveLet = let+  local 0 = 1+  local n = n * local (n - 1)+  {-# INLINE local #-}+ in+  local++localMutual :: Int -> Int+localMutual = local where+  local 0 = 1+  local n = n * local' (n - 1)+  {-# INLINE local #-}++  local' = local+  {-# NOINLINE local' #-}
+ test/PragmaDetectionSpec.hs view
@@ -0,0 +1,34 @@+{-# OPTIONS_GHC -O2 #-}++{-# LANGUAGE TemplateHaskell #-}++module PragmaDetectionSpec (spec) where++import TestUtils+++------------------------------------------------------------------------------+spec :: Spec+spec = describe "plugin" $ do+  it "should respect INLINE pragma" $ do+    shouldFail    $(inspectTest $ 'recursiveDefault === 'mutual)+    shouldSucceed $(inspectTest $ 'recursiveOn      === 'mutual)++------------------------------------------------------------------------------+recursiveDefault :: Int -> Int+recursiveDefault 0 = 1+recursiveDefault n = n * recursiveDefault (n - 1)++recursiveOn :: Int -> Int+recursiveOn 0 = 1+recursiveOn n = n * recursiveOn (n - 1)+{-# INLINE recursiveOn #-}++mutual :: Int -> Int+mutual 0 = 1+mutual n = n * mutual' (n - 1)+{-# INLINE mutual #-}++mutual' :: Int -> Int+mutual' = mutual+{-# NOINLINE mutual' #-}
+ test/Spec.hs view
@@ -0,0 +1,1 @@+{-# OPTIONS_GHC -F -pgmF hspec-discover #-}
+ test/TestUtils.hs view
@@ -0,0 +1,18 @@+module TestUtils+  ( module U+  , shouldSucceed+  , shouldFail+  ) where++import Test.Hspec      as U+import Test.Inspection as U+++------------------------------------------------------------------------------+shouldSucceed :: Result -> Expectation+shouldSucceed r = r `shouldSatisfy` \case Success{} -> True+                                          Failure e -> error e++shouldFail :: Result -> Expectation+shouldFail r = r `shouldSatisfy` \case Success m -> error m+                                       Failure{} -> True