Hi,

I think most of the docs about exitification are the notes in the
Exitify module, and then there is the original ticket at
https://gitlab.haskell.org/ghc/ghc/issues/14152

I don’t immediately see the connection to SpecConstr on function
values, though, so I don't really know what’s tickling your neurons
right now.

Cheers,
Joachim


Am Dienstag, den 31.03.2020, 22:49 +0000 schrieb Simon Peyton Jones:
> Joachim: this conversation is triggering some hind-brain neurons
> related to exitification, or something like that.  I recall that we
> discovered we could get some surprising fusion of recursive functions
> expressed as  join points.  Something like   f . g . h
> where h loops for a while and returns, and same for g and f.  Then
> the call to g landed up in the return branch of h, and same for f.
>  
> But I can’t find anything in writing.  The Exitify module doesn’t say much. I 
> thought we had a wiki page but I can’t find it.  Can you remember?
>  
> Thanks
>  
> Simon
>  
> From: Alexis King <lexi.lam...@gmail.com> 
> Sent: 31 March 2020 22:18
> To: Sebastian Graf <sgraf1...@gmail.com>; Simon Peyton Jones 
> <simo...@microsoft.com>
> Cc: ghc-devs <ghc-devs@haskell.org>
> Subject: Re: Fusing loops by specializing on functions with SpecConstr?
>  
> Sebastian and Simon,
>  
> Thank you both for your responses—they are all quite helpful! I agree with 
> both of you that figuring out how to do this kind of specialization without 
> any guidance from the programmer seems rather intractable. It’s too hard to 
> divine where it would actually be beneficial, and even if you could, it seems 
> likely that other optimizations would get in the way of it actually working 
> out.
>  
> I’ve been trying to figure out if it would be possible to help the optimizer 
> out by annotating the program with special combinators like the existing ones 
> provided by GHC.Magic. However, I haven’t been able to come up with anything 
> yet that seems like it would actually work.
>  
> > On Mar 31, 2020, at 06:12, Simon Peyton Jones <simo...@microsoft.com> wrote:
> >  
> > Wow – tricky stuff!   I would never have thought of trying to optimise that 
> > program, but it’s fascinating that you get lots and lots of them from FRP.
> 
>  
> For context, the reason you get all these tiny loops is that arrowized FRP 
> uses the Arrow and ArrowChoice interfaces to build its programs, and those 
> interfaces use tiny combinator functions like these:
>  
>     first :: Arrow a => a b c -> a (b, d) (c, d)
>     (***) :: Arrow a => a b d -> a c e -> a (b, c) (d, e)
>     (|||) :: ArrowChoice a => a b d -> a c d -> a (Either b c) d
>  
> This means you end up with programs built out of dozens or hundreds of uses 
> of these tiny combinators. You get code that looks like
>  
>     first (left (arr f >>> g ||| right h) *** second i)
>  
> and this is a textbook situation where you want to specialize and inline all 
> the combinators! For arrows without this tricky recursion, doing that works 
> as intended, and GHC’s simplifier will do what it’s supposed to, and you get 
> fast code.
>  
> But with FRP, each of these combinators is recursive. This means you often 
> get really awful code that looks like this:
>  
>     arr (\case { Nothing -> Left (); Just x -> Right x }) >>> (f ||| g)
>  
> This converts a Maybe to an Either, then branches on it. It’s analogous to 
> writing something like this in direct-style code:
>  
>     let y = case x of { Nothing -> Left (); Just x -> Right x }
>     in case y of { Left () -> f; Right x -> g x }
>  
> We really want the optimizer to eliminate the intermediate Either and just 
> branch on it directly, and if GHC could fuse these tiny recursive loops, it 
> could! But without that, all this pointless shuffling of values around 
> remains in the optimized program.
> 
> 
> >  I wonder whether it’d be possible to adjust the FRP library to generate 
> > easier-to-optimise code. Probably not, but worth asking.
> 
>  
> I think it’s entirely possible to somehow annotate these combinators to 
> communicate this information to the optimizer, but I don’t know what the 
> annotations ought to look like. (That’s the research part!)
>  
> But I’m not very optimistic about getting the library to generate 
> easier-to-optimize code with the tools available today. Sebastian’s example 
> of SF2 and stream fusion sort of works, but in my experience, something like 
> that doesn’t handle enough cases well enough to work on real arrow programs.
>  
> >  Unrolling one layer of a recursive function.  That seems harder: how we 
> > know to *stop* unrolling as we successively simplify?  One idea: do one 
> > layer of unrolling by hand, perhaps even in FRP source code:
> > add1rec = SF (\a -> let !b = a+1 in (b,add1rec))
> > add1 = SF (\a -> let !b = a+1 in (b,add1rec))
> 
>  
> Yes, I was playing with the idea at one point of some kind of RULE that 
> inserts GHC.Magic.inline on the specialized RHS. That way the programmer 
> could ask for the unrolling explicitly, as otherwise it seems unreasonable to 
> ask the compiler to figure it out.
>  
> > On Mar 31, 2020, at 08:08, Sebastian Graf <sgraf1...@gmail.com> wrote:
> >  
> > We can formulate SF as a classic Stream that needs an `a` to produce its 
> > next element of type `b` like this (SF2 below)
> 
>  
> This is a neat trick, though I’ve had trouble getting it to work reliably in 
> my experiments (even though I was using GHC.Types.SPEC). That said, I also 
> feel like I don’t understand the subtleties of SpecConstr very well, so it 
> could have been my fault.
>  
> The more fundamental problem I’ve found with that approach is that it doesn’t 
> do very well for arrow combinators like (***) and (|||), which come up very 
> often in arrow programs but rarely in streaming. Fusing long chains of 
> first/second/left/right is actually pretty easy with ordinary RULEs, but 
> (***) and (|||) are much harder, since they have multiple continuations.
>  
> It seems at first appealing to rewrite `f *** g` into `first f >>> second g`, 
> which solves the immediate problem, but this is actually a lot less efficient 
> after repeated rewritings. You end up rewriting `(f ||| g) *** h` into `first 
> (left f) >>> first (right g) >>> second h`, turning two distinct branches 
> into four, and larger programs have much worse exponential blowups.
>  
> So that’s where I’ve gotten stuck! I’ve been toying with the idea of thinking 
> about expression “shells”, so if you have something like
>  
>     first (a ||| b) >>> c *** second (d ||| e) >>> f
>  
> then you have a “shell” of the shape
>  
>     first (● ||| ●) >>> ● *** second (● ||| ●) >>> ●
>  
> which theoretically serves as a key for the specialization. You can then 
> generate a specialization and a rule:
>  
>     $s a b c d e f = ...
>     {-# RULE forall a b c d e f.
>             first (a ||| b) >>> c *** second (d ||| e) >>> f = $s a b c d e f 
> #-}
>  
> The question then becomes: how do you specify what these shells are, and how 
> do you specify how to transform the shell into a specialized function? I 
> don’t know, but it’s something a Core plugin could theoretically do. Maybe it 
> makes sense for this domain-specific optimization to be a Core pass that runs 
> before the simplifier, like the typeclass specializer currently is, but I 
> haven’t explored that yet.
>  
> Alexis
-- 
Joachim Breitner
  m...@joachim-breitner.de
  http://www.joachim-breitner.de/


_______________________________________________
ghc-devs mailing list
ghc-devs@haskell.org
http://mail.haskell.org/cgi-bin/mailman/listinfo/ghc-devs

Reply via email to