The goal of stream fusion is to eliminate constructors of
internal state used in a stream. For example, in case of
streamly streams, the
constructors of Step type, Yield, Skip and Stop would get
eliminated by fusion. Similarly, constructors of any other intermediate
state types get eliminated when stream fusion works correctly. See the papers
in the reference section for more details on stream fusion.
Stream fusion depends on the GHC case-of-case transformations eliminating intermediate constructors. Case-of-case transformation in turn depends on inlining. During core-to-core transformations GHC may create several internal bindings (e.g. join points) which may not get inlined because their size is too big. Even though we know that after fusion the resulting code would be smaller and more efficient. The programmer cannot force inlining of these bindings as there is no way for the programmer to address these bindings at the source level because they are internal, generated during core-to-core transformations. As a result stream fusion fails unpredictably depending on whether GHC was able to inline the internal bindings or not.
See GHC ticket #17075 for more details.
This plugin provides the programmer with a way to annotate certain
types using a Fuse pragma from the
fusion-plugin-types
package. The programmer would annotate the types that are to be
eliminated by fusion. During the simplifier phase the plugin goes
through the relevant bindings and if one of these types are found
inside a binding then that binding is marked to be inlined
irrespective of the size.
This plugin was primarily motivated by streamly but it can be used in general.
There are two different stages where the plugin functionality is used, (1) when
writing code for fusion we annotate data types or bindings using annotations
imported from the fusion-plugin-types packages, (2) when compiling the code
we use the fusion-plugin package as a compiler plugin to make those
annotations work towards fusing the code better.
See the fusion-plugin-types package for detailed reference on available
annotations.
Library authors annotate the data types used in the intermediate states of
stream pipelines using the Fuse annotation. All the functions where these
data types appear are liable to be force inlined by the plugin to make the
types fuse.
Fuse marks a type as fusible everywhere it is used. Sometimes a type should
drive fusion only inside one particular function and must not force inlining
wherever else it happens to be used. For that, annotate the binding (not the
type) with FuseTypes from Fusion.Plugin.Types. The listed types then behave
exactly as if they carried a Fuse annotation, but only while inlining inside
that one binding:
{-# LANGUAGE TemplateHaskellQuotes #-}
import Fusion.Plugin.Types (FuseTypes(..))
{-# ANN function (FuseTypes [''Step, ''Maybe]) #-}
function :: ...Type references are TH Names (''Step), so a typo or a stale reference to a
renamed type is a compile error rather than a silently-ignored annotation.
Using ''Foo requires {-# LANGUAGE TemplateHaskellQuotes #-} (or the heavier
TemplateHaskell) in the annotated module.
NoFuseTypes is the inverse of Fuse/FuseTypes. Sometimes a type should
drive fusion in general (via a module-wide Fuse annotation) but must not
force inlining inside one particular function. Annotate that binding with
NoFuseTypes from Fusion.Plugin.Types: the listed types then behave as if
they carried no Fuse annotation, disabling the forced inlining they would
otherwise drive, but only while inlining inside that one binding. Fusion of
those types everywhere else in the module is unaffected:
{-# LANGUAGE TemplateHaskellQuotes #-}
import Fusion.Plugin.Types (NoFuseTypes(..))
{-# ANN function (NoFuseTypes [''Step, ''Maybe]) #-}
function :: ...NoFuse is the blanket form of NoFuseTypes: instead of listing types,
annotate the binding with NoFuse from Fusion.Plugin.Types to disable
forced inlining for that binding altogether, regardless of which types are
involved. This overrides any module-wide Fuse annotation and any
FuseTypes/NoFuseTypes on the same binding:
import Fusion.Plugin.Types (NoFuse(..))
{-# ANN function NoFuse #-}
function :: ...To verify elimination of certain types within a function annotate
the function with InspectConstructions (types constructed/allocated)
and/or InspectPatternMatches (types scrutinized in a case). If
a violation is found the plugin will complain providing details of the
violation. When the werror plugin option is used it will fail the
compilation on violation.
The two annotation types are independent, so a single binding may carry
one of each to inspect both the constructing and the scrutinizing
position at once. All the examples below use the construction variant;
each has an analogous ...PatternMatches counterpart.
{-# LANGUAGE TemplateHaskellQuotes #-}
import Fusion.Plugin.Types (InspectConstructions(..), InspectPatternMatches(..))The whole system reduces to four simple rules:
- A type explicitly listed in a
Forbid...annotation is always reported (forbidden). - A type explicitly listed in a
Permit...annotation is always allowed (permitted), never reported. - The
forbid-fusedplugin option adds allFuseannotated types to the forbidden set. - By default unboxed types are implicitly in the permitted list. The
inspect-unboxedplugin option means unboxed types are no longer implicitly permitted.
Disallow only the specified types and allow the rest, irrespective of the
Fuse annotation. To check both constructions and pattern matches, attach one
annotation of each type to the binding.
{-# ANN function (ForbidConstructions [''Step]) #-}
{-# ANN function (ForbidPatternMatches [''Step]) #-}
function2 :: ...Additionally pass the forbid-fused plugin option to forbid all Fuse
annotated types too, using them as a baseline; the types named in the
annotation are then forbidden on top of them. With this option
ForbidConstructions [] forbids exactly the Fuse annotated types.
ghc-options: -fplugin-opt=Fusion.Plugin:forbid-fused
Report the listed types only where they are constructed:
{-# ANN function (ForbidConstructions [''Step]) #-}Report the listed types only where they are pattern-matched:
{-# ANN function (ForbidPatternMatches [''Step]) #-}Allow only the specified types and disallow all others. To check both constructions and pattern matches, attach one annotation of each type to the binding.
{-# ANN function (PermitConstructions [''Int, ''IO]) #-}
{-# ANN function (PermitPatternMatches [''Int, ''IO]) #-}
function3 :: ...To report every constructed type used within a function:
{-# ANN function (PermitConstructions []) #-}
function4 :: ...Report every constructed type except the listed ones:
{-# ANN function (PermitConstructions [''Int, ''IO]) #-}Report every pattern-matched type except the listed ones:
{-# ANN function (PermitPatternMatches [''Int, ''IO]) #-}If any of the types in the permitted list is not actually found in the binding, a warning is emitted so that stale entries can be removed from the list.
By default unboxed types (e.g. Int#, unboxed tuples and sums) are
implicitly considered in the Permit... list.
When inspect-unboxed plugin option is used, unboxed types are no longer
implicitly permitted, you will have to mention them explicitly in the permitted
list to allow them, forbidden list is not affected by this option.
ghc-options: -fplugin-opt=Fusion.Plugin:inspect-unboxed
To check the presence or absence of type classes in the Core of a binding,
annotate it with InspectTypeClasses. A type class appears in Core as a
dictionary argument; a class that survives to Core usually means a dictionary
that failed to specialize away.
{-# LANGUAGE TemplateHaskellQuotes #-}
import Fusion.Plugin.Types (InspectTypeClasses(..))'Num' should not appear in the core.
{-# ANN function1 (ForbidTypeClasses [''Num]) #-}
function1 :: ...'Ord' and 'Eq' can appear but no other type class can be present.
{-# ANN function2 (PermitTypeClasses [''Ord, ''Eq]) #-}
function2 :: ...If any of the permitted type classes is not actually found in the binding, a warning is emitted so that stale entries can be removed from the list.
Sometimes the Core blows up due to aggressive inlining and SpecConstr
optimizations. To guard against or detect such issues we can use the
MaxCoreSize as a core size ratchet for a function.
import Fusion.Plugin.Types (MaxCoreSize(..))
{-# ANN function (MaxCoreSize 1000) #-}
function :: ...This prints a line such as:
fusion-plugin: function: core size (1361 terms) exceeds the specified size (1000 terms).
Use DumpCore annotation to write the final simplified core of a function to a
file in the compiler's dump directory (as set by -dumpdir), or under
fusion-plugin-output/<package-name> when no dump directory is set:
{-# ANN function DumpCore #-}
function :: ...To understand how the core of a function evolves through the optimizer, use the
DumpCorePasses annotation. It is the per-binding counterpart of the
dump-core option (see Examining Optimization Passes).
Instead of dumping the whole module after each pass it dumps only the annotated
binding, writing one file per pass named
<module>.<binding>.<NN-pass>.dump-simpl:
{-# ANN function DumpCorePasses #-}
function :: ...To make the fusion annotations do the actual work you need to compile
your code with the fusion-plugin added to GHC during compilation. To do
that add this package to the build-depends in the cabal file of the
package to be compiled and use the following ghc options:
ghc-options: -O2 -fplugin=Fusion.Plugin
-fplugin-opt=Fusion.Plugin:verbose=1: report unfused functions. Verbosity
levels 2, 3, 4 can be used for more verbose output.
-fplugin-opt=Fusion.Plugin:werror: treat annotation-check violations as
errors instead of warnings. This option is useful in CI builds to fail on
violation.
To record the core size of every binding that carries a violation-causing
annotation (InspectPatternMatches, InspectConstructions, InspectTypeClasses
or MaxCoreSize) to a file, pass the dump-core-sizes option:
ghc-options: -fplugin-opt=Fusion.Plugin:dump-core-sizes
Each module then writes a <module-name>.core-sizes.csv file in the
compiler's dump directory (as set by -dumpdir), or in
fusion-plugin-output/<package-name> when no dump directory is set. The file
starts with a name,core-size header row followed by one row per annotated
binding.
This option is useful to collect the core sizes before and after a change and automatically report the changes in a CI.
By default the CSV is truncated on each compilation. Pass csv-append to keep
the existing file and append each run's header and rows instead, accumulating
successive builds in one file:
ghc-options: -fplugin-opt=Fusion.Plugin:dump-core-sizes -fplugin-opt=Fusion.Plugin:csv-append
Note: dump-core does not work for GHC-9.0.x.
-fplugin-opt=Fusion.Plugin:dump-core: dump core after each
core-to-core transformation. Output from each transformation is printed
in a different file. This is useful to examine where an optimization failed or
occurred.
The files are written under the compiler's dump directory
as set by -dumpdir, or fusion-plugin-output/<package-name>
otherwise. Each pass adds a <NN-pass-name>.dump-simpl suffix, e.g.
Stream.Type.Basic.07-After-simplifier.dump-simpl.
dump-core-if-annotated option dumps the core of every binding
that carries a violation-causing annotation (InspectPatternMatches,
InspectConstructions, InspectTypeClasses or MaxCoreSize), without adding a
DumpCore annotation to each one:
ghc-options: -fplugin-opt=Fusion.Plugin:dump-core-if-annotated
Use dump-core-if-violated to dump the core of those annotated
bindings for which a check actually reported a violation:
ghc-options: -fplugin-opt=Fusion.Plugin:dump-core-if-violated
This option is useful to examine the core to find the reported
violations. dump-core-if-annotated can be used in a baseline CI build
to dump cores of all annotated bindings, and the dump-core-if-violated
can be used to dump cores of violations in a changed build which can be
diffed against the baseline cores.
If you are a library author looking to annotate the types, you need to use the fusion-plugin-types package.
All contributions are welcome! The code is available under Apache-2.0 license on github. In case you have any questions or suggestions please contact the maintainers.
We would be happy to see this work getting integrated with GHC as a fix for GHC ticket #17075, any help with that would be appreciated.