GHC optimization opportunity? (from Monad of No Return thread)

(This is forked off of Monad of No Return: Issues with `(>>) = (*>)` - #18 by L0neGamer, since discussing how to optimize common implementations of (*>) isn’t quite on topic there.)

So here’s the Core produced by @effectfully’s example for (*>):

lvl5 = \ @b -> One id

Rec {
$fApplicativeNonEmpty2
  = \ @b @a ds ->
      case ds of {
        One x -> lvl5;
        Cons x xs -> Cons id ($fApplicativeNonEmpty2 xs)
      }
end Rec }

$fApplicativeNonEmpty_$c*>
  = \ @a @b eta eta1 ->
      letrec {
        go
          = \ ds ->
              case ds of {
                One f2 ->
                  letrec {
                    go1
                      = \ ds1 ->
                          case ds1 of {
                            One x -> One (f2 x);
                            Cons x xs -> Cons (f2 x) (go1 xs)
                          }; } in
                  go1 eta1;
                Cons f2 fs ->
                  let { ys = go fs } in
                  letrec {
                    go1
                      = \ ds1 ->
                          case ds1 of {
                            One x -> Cons x ys;
                            Cons x xs -> Cons x (go1 xs)
                          }; } in
                  letrec {
                    go2
                      = \ ds1 ->
                          case ds1 of {
                            One x -> One (f2 x);
                            Cons x xs -> Cons (f2 x) (go2 xs)
                          }; } in
                  go1 (go2 eta1)
              }; } in
      go ($fApplicativeNonEmpty2 eta)

There’s an opportunity for a case-of-case transformation to be applied to the composition of go and $fApplicativeNonEmpty2, though I expect it isn’t being taken because both of those functions are recursive? But if we inline and apply it by hand, we get this:

$fApplicativeNonEmpty_$c*>
  = \ @a @b eta eta1 ->
      letrec {
        go
          = \ ds ->
              case ds of {
                One f2 ->
                  letrec {
                    go1
                      = \ ds1 ->
                          case ds1 of {
                            One x -> One (f2 x);
                            Cons x xs -> Cons (f2 x) (go1 xs)
                          }; } in
                  go1 eta1;
                Cons f2 fs ->
                  let { ys = go fs } in
                  letrec {
                    go1
                      = \ ds1 ->
                          case ds1 of {
                            One x -> Cons x ys;
                            Cons x xs -> Cons x (go1 xs)
                          }; } in
                  letrec {
                    go2
                      = \ ds1 ->
                          case ds1 of {
                            One x -> One (f2 x);
                            Cons x xs -> Cons (f2 x) (go2 xs)
                          }; } in
                  go1 (go2 eta1)
              }; } in
      case eta of {
        One x -> letrec {
                    go1 -- LOOK HERE
                      = \ ds1 ->
                          case ds1 of {
                            One x1 -> One (id x1);
                            Cons x1 xs -> Cons (id x1) (go1 xs)
                          }; } in
                  go1 eta1;
        Cons x xs ->
                  let { ys = go ($fApplicativeNonEmpty2 xs) } in
                  letrec {
                    go1
                      = \ ds1 ->
                          case ds1 of {
                            One x1 -> Cons x1 ys;
                            Cons x1 xs1 -> Cons x1 (go1 xs1)
                          }; } in
                  letrec {
                    go2
                      = \ ds1 ->
                          case ds1 of {
                            One x1 -> One (id x1);
                            Cons x1 xs1 -> Cons (id x1) (go2 xs1)
                          }; } in
                  go1 (go2 eta1)
      }

Then, all we have to do is notice that the go1 marked with -- LOOK HERE is the identity function (unless we are -fpedantic-bottoms). This is again probably an issue because go1 is also recursive, but GHC already does this sort of thing for non-recursive functions.

If we can do those two optimizations, that’s enough for (*>) to be tail-recursive in its second operand if its first is One. Nothing about this is specialized to anything from Functor or Applicative, and no new language features need to be added. GHC just has to try things it already does in the context of non-recursive functions in a recursive context.

I say ‘just’, but, uh, I imagine there are good reasons why this hasn’t been supported yet. How hard would these particular extensions be, folks who know?

4 Likes

I say ‘just’, but, uh, I imagine there are good reasons why this hasn’t been supported yet. How hard would these particular extensions be, folks who know?

GHC currently never inlines a recursive function; in this case both go and $fApplicativeNonEmpty2 are recursive.

Inlining recursive functions is tricky because you can just inline them again, … and again. It’s hard to know when to stop.

The Simplifier (which does inlining) is applied repeatedly to the program (after each optimisation pass, for example) so it’s hard for it to remember “I have already inlined this function once”.

Even if you did inline a recursive function, the danger might be that you make the first iteration more efficient, but then end up calling a non-inlined version for all the other iterations; result is code bloat without much perf impact.

But the path lies open for someone to design a clever Core-to-Core plugin!

4 Likes

See #22902: Optimization opportunity: hidden identity functions. · Issues · Glasgow Haskell Compiler / GHC · GitLab

3 Likes