[GHC] #14152: Float exit paths out of recursive functions

GHC ghc-devs at haskell.org
Thu Aug 24 23:16:40 UTC 2017


#14152: Float exit paths out of recursive functions
-------------------------------------+-------------------------------------
        Reporter:  nomeata           |                Owner:  (none)
            Type:  task              |               Status:  new
        Priority:  normal            |            Milestone:
       Component:  Compiler          |              Version:  8.2.1
      Resolution:                    |             Keywords:  JoinPoints
Operating System:  Unknown/Multiple  |         Architecture:
                                     |  Unknown/Multiple
 Type of failure:  None/Unknown      |            Test Case:
      Blocked By:                    |             Blocking:
 Related Tickets:  #14137 #10918     |  Differential Rev(s):
       Wiki Page:                    |
-------------------------------------+-------------------------------------
Changes (by simonpj):

 * keywords:   => JoinPoints


Old description:

> This is a spin off of a discussion from #14137.
>
> == The problem ==
>
> We generally avoid inlining or floating a binding into a recursive
> function, because we do not want to duplicat work/allocations.
>
> But sometimes the binding is only used inside the recursive function on
> the “exit path”. In which case it would be good to inline. Example:
>
> {{{#!hs
>   let x = f x0
>   in let go 10 = h x
>          go i = go (i+1)
>      in go (0::Int) + 100
> }}}
>
> It would be beneficial to inline `x`.
>
> The problem is that it is not very obvious that this occurence of `x` is
> ok for inlining. In particular, it is not syntactically visible.
>
> == Proposed solution ==
>
> If we apply loopification (#14068), this would turn into
>
> {{{#!hs
>   let x = f x0
>   in let go n =
>        joinrec jgo 10 = h x
>                jgo i = call jgo (i+1)
>        in call jgo n
>      in go (0::Int) + 100
> }}}
>
> I’d like to call this ''first joinrec normal form'', defined as “every
> recursive function where all recursive calls are tail-recursive is a
> recursive join point”.
>
> This ticket proposes to transform this even further and ''float out all
> case alternatives that do not mention `jgo` as non-recursive join-
> points'', as so:
>
> {{{#!hs
>   let x = f x0
>   in let go n =
>        join jexit = h x
>        joinrec jgo 10 = call jexit
>                jgo i = call jgo (i+1)
>        in call jgo n
>      in go (0::Int) + 100
> }}}
>
> I’d like to call this ''second `joinrec` normal form'', defined as “in
> first `joinrec` normal form, and all subexpressions of a recursive join
> point `j` that are in tail-call position and do not mention `j` are join
> calls”.
>
> If the floated expression has free variables that are bound inside the
> `joinrec`, they turn into parameters of the newly created joinpoint.
>
> At this point, GHC can tell that `go` is called at most once, and will
> therefore happily inline `x` into the right hand side of `jexit.
>
> == Alternative solutions ==
>
> Ticket #10918 uses Call Arity results to learn that `x` is one-Shot, and
> inline it even in the original code. This works, but the problem is that
> float-out will undo this. See [ticket:10918#comment:5].
>
> == Limitation ===
>
> It only works for recursive functions that are join points, or can be
> turned into join points by loopification (#14068). It does not work
> forexample for
>
> {{{#!hs
> let x = f x0
> let go 0 = h x
>     go n = (go (n-1) + 1
> in go 10
> }}}
>
> although it would be equally desirable to float  `h x` out of `go` so
> that `x` can be inlined.
>
> == Preservation ==
>
> A remaining tricky point is that we need to stop one of these carefully-
> constructed non-recursive join points being inlined into a recursive join
> point, even if it is invoked at just one place. That should not be hard.
> And in a final run of the simplifer (or in CorePrep) we could switch off
> that restriction and let it inline. (Ticket #14137 is about inlining
> ''more'' join points into recursive join points, so it is the antithesis
> to the present ticket.)

New description:

 This is a spin off of a discussion from #14137.

 == The problem ==

 We generally avoid inlining or floating a binding into a recursive
 function, because we do not want to duplicat work/allocations.

 But sometimes the binding is only used inside the recursive function on
 the “exit path”. In which case it would be good to inline. Example:

 {{{#!hs
   let x = f x0
   in let go 10 = h x
          go i = go (i+1)
      in go (0::Int) + 100
 }}}

 It would be beneficial to inline `x`.

 The problem is that it is not very obvious that this occurence of `x` is
 ok for inlining. In particular, it is not syntactically visible.

 == Proposed solution ==

 If we apply loopification (#14068), this would turn into

 {{{#!hs
   let x = f x0
   in let go n =
        joinrec jgo 10 = h x
                jgo i = call jgo (i+1)
        in call jgo n
      in go (0::Int) + 100
 }}}

 I’d like to call this ''first joinrec normal form'', defined as “every
 recursive function where all recursive calls are tail-recursive is a
 recursive join point”.

 This ticket proposes to transform this even further and ''float out all
 case alternatives that do not mention `jgo` as non-recursive join-
 points'', as so:

 {{{#!hs
   let x = f x0
   in let go n =
        join jexit = h x
        joinrec jgo 10 = call jexit
                jgo i = call jgo (i+1)
        in call jgo n
      in go (0::Int) + 100
 }}}

 I’d like to call this ''second `joinrec` normal form'', defined as “in
 first `joinrec` normal form, and all subexpressions of a recursive join
 point `j` that are in tail-call position and do not mention `j` are join
 calls”.

 If the floated expression has free variables that are bound inside the
 `joinrec`, they turn into parameters of the newly created joinpoint.

 At this point, GHC can tell that `go` is called at most once, and will
 therefore happily inline `x` into the right hand side of `jexit.

 == Alternative solutions ==

 Ticket #10918 uses Call Arity results to learn that `x` is one-Shot, and
 inline it even in the original code. This works, but the problem is that
 float-out will undo this. See [ticket:10918#comment:5].

 == Limitation ==

 It only works for recursive functions that are join points, or can be
 turned into join points by loopification (#14068). It does not work
 forexample for

 {{{#!hs
 let x = f x0
 let go 0 = h x
     go n = (go (n-1) + 1
 in go 10
 }}}

 although it would be equally desirable to float  `h x` out of `go` so that
 `x` can be inlined.

 == Preservation ==

 A remaining tricky point is that we need to stop one of these carefully-
 constructed non-recursive join points being inlined into a recursive join
 point, even if it is invoked at just one place. That should not be hard.
 And in a final run of the simplifer (or in CorePrep) we could switch off
 that restriction and let it inline. (Ticket #14137 is about inlining
 ''more'' join points into recursive join points, so it is the antithesis
 to the present ticket.)

--

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


More information about the ghc-tickets mailing list