[GHC] #13966: Skip-less stream fusion

GHC ghc-devs at haskell.org
Tue Jul 18 12:33:16 UTC 2017


#13966: Skip-less stream fusion
-------------------------------------+-------------------------------------
        Reporter:  jmspiewak         |                Owner:  (none)
            Type:  feature request   |               Status:  new
        Priority:  normal            |            Milestone:
       Component:  Compiler          |              Version:  8.2.1-rc3
      Resolution:                    |             Keywords:  JoinPoints
Operating System:  Unknown/Multiple  |         Architecture:
                                     |  Unknown/Multiple
 Type of failure:  None/Unknown      |            Test Case:
      Blocked By:                    |             Blocking:
 Related Tickets:                    |  Differential Rev(s):
       Wiki Page:                    |
-------------------------------------+-------------------------------------

Comment (by simonpj):

 This is a very interesting example, thank you.

 I note that in HEAD it fuses just fine.  I have not yet figured out
 exactly why, but I want to look at this code, from teh Description:
 {{{
 letrec {
   $wloop [InlPrag=[0], Occ=LoopBreaker] :: Int# -> Step1 Int Int
   [LclId, Arity=1, Str=<S,U>, Unf=OtherCon []]
   $wloop
     = \ (ww1_s9e8 :: Int#) ->
         case tagToEnum# @ Bool (># ww1_s9e8 ww_s9ep) of {
           False ->
             case remInt# ww1_s9e8 2# of {
               __DEFAULT -> $wloop (+# ww1_s9e8 1#);
               0# ->
                 Main.Yield1
                   @ Int @ Int (GHC.Types.I# (+# ww1_s9e8 1#))
 (GHC.Types.I# ww1_s9e8)
             };
           True -> Main.Done1 @ Int @ Int
         }; } in
 joinrec {
   $wloop1 [InlPrag=[0], Occ=LoopBreaker] :: Int# -> Int# -> Int#
   [LclId[JoinId(2)], Arity=2, Str=<S,U><S,U>, Unf=OtherCon []]
   $wloop1 (ww1_s9ef :: Int#) (ww2_s9ej :: Int#)
     = case $wloop ww2_s9ej of {
         Done1 -> ww1_s9ef;
         Yield1 s'_a497 x_a498 ->
           case x_a498 of { GHC.Types.I# y_a66i ->
           case s'_a497 of { GHC.Types.I# ww4_X9hA ->
           jump $wloop1 (+# ww1_s9ef y_a66i) ww4_X9hA
           }
           }
       }; } in
 jump $wloop1 0# 1#
 }}}
 Once GHC gets the program into this state, it's not going to be able to
 optimise it.   HEAD somehow avoids this dead end, but I hate things where
 GHC can get stuck in a dead end.  I think this code ''ought'' to optimise
 just fine.  Here's why.

 Look at thar functionn `$wloop`.   It's not a join point becuaes it's not
 tail-called in the body of the `letrec`.   But suppose we transform
 `$wloop` like this:
 {{{
 let {
   $wloop [InlPrag=[0], Occ=LoopBreaker] :: Int# -> Step1 Int Int
   [LclId, Arity=1, Str=<S,U>, Unf=OtherCon []]
   $wloop
     = \x. joinrec $j (ww1_s9e8 :: Int#)
             = case tagToEnum# @ Bool (># ww1_s9e8 ww_s9ep) of {
                 False ->
                   case remInt# ww1_s9e8 2# of {
                     __DEFAULT -> jump $j (+# ww1_s9e8 1#);
                     0# ->
                       Main.Yield1
                         @ Int @ Int (GHC.Types.I# (+# ww1_s9e8 1#))
 (GHC.Types.I# ww1_s9e8)
                   };
                 True -> Main.Done1 @ Int @ Int
           in jump $j x
 }}}
 Now `$wloop` is non-recursive, so we can inline it at its only call site,
 in `$wloop1`:
 {{{
 joinrec {
   $wloop1 [InlPrag=[0], Occ=LoopBreaker] :: Int# -> Int# -> Int#
   [LclId[JoinId(2)], Arity=2, Str=<S,U><S,U>, Unf=OtherCon []]
   $wloop1 (ww1_s9ef :: Int#) (ww2_s9ej :: Int#)
     = case (...the body of $wloop...) of {
         Done1 -> ww1_s9ef;
         Yield1 s'_a497 x_a498 ->
           case x_a498 of { GHC.Types.I# y_a66i ->
           case s'_a497 of { GHC.Types.I# ww4_X9hA ->
           jump $wloop1 (+# ww1_s9ef y_a66i) ww4_X9hA
           }
           }
       }; } in
 jump $wloop1 0# 1#
 }}}
 And now the right fusion will happen.

 The crucial bit was the transformation of `$wloop`: we took a tail-
 recursive function, introduced a (recursive) join point into it, which
 made it non-recursive.  Even if nothing further happens, the
 implementation of `$wloop` is a bit more efcient because the tail call is
 just a branch.  But the big thing here is that we can now inline `$wloop`
 in `$wloop1`.

 So the idea is this: a transformation to turn a tail-recursive function
 definition into one that is implemented with a recursive join point.  If
 we had such a transformation, it'd get us out of the dead end.

 Actually, there's a variant of the Static Argument Transformation
 ([wiki:StaticArgumentTransformation]) at work here.  Consider
 {{{
 f x y = case y of
           A -> f x y'
           B -> e2
           C -> e3
 }}}
 Here the first argument to `f` is "static"; that is, the same in every
 call.  So we can transform like this
 {{{
 f x y = joinrec $j y = case y of
                           A -> $j y'
                           B -> e2
                           C -> e3
         in $j y
 }}}
 Note that `x` isn't passed around in every iteration.

 There's a GHC module `SAT.hs` which does the static argument
 transformation, but it is not join-point aware.  So we should fix that.

 One reason we don't currently do SAT all the time is that the results are
 a bit ambiguous; see Andre Santos's thesis for more, cited on
 [wiki:StaticArgumentTransformation].  BUT I think that some (maybe most)
 of the problems with SAT may go away if we restrict SAT to tail-recursive
 functions that we can turn into `joinrecs`.

 Any volunteers?

-- 
Ticket URL: <http://ghc.haskell.org/trac/ghc/ticket/13966#comment:8>
GHC <http://www.haskell.org/ghc/>
The Glasgow Haskell Compiler


More information about the ghc-tickets mailing list