Note [WW for calling convention]
If we know a function f will always evaluate a particular argument we might decide that it should rather get evaluated by the caller. We call this "unlifting" the argument. Sometimes the caller knows that the argument is already evaluated, so we won't generate any code to enter/evaluate the argument. This evaluation avoidance can be quite beneficial. Especially for recursive functions who pass the same lifted argument along on each iteration or walk over strict data structures. One way to achieve this is to do a W/W split, where the wrapper does the evaluation, and the worker can treat its arguments as unlifted. The wrapper is small and will be inlined at almost all call sites and the evaluation code in the wrapper can then cancel out with evaluation done by the calling context if the argument is evaluated there. Same idea as W/W to avoid allocation really, just for a different kind of work. Performing W/W might not always be a win. In particular it's easy to break (badly written, but common) rule frameworks by doing additional W/W splits. See #20364 for a more detailed explanation. Hence we have the following strategies with different trade-offs: A) Never do W/W *just* for unlifting of arguments. + Very conservative - doesn't break any rules - Lot's of performance left on the table B) Do W/W on just about anything where it might be beneficial. + Exploits pretty much every opportunity for unlifting. - A bit of compile time/code size cost for all the wrappers. - Can break rules which would otherwise fire. See #20364. C) Unlift *any* (non-boot exported) functions arguments if they are strict. That is instead of creating a Worker with the new calling convention we change the calling convention of the binding itself. + Exploits every opportunity for unlifting. + Maybe less bad interactions with rules. - Requires tracking of boot-exported definitions. - Requires either: ~ Eta-expansion at *all* call sites in order to generate an impedance matcher function. Leading to massive code bloat. Essentially we end up creating a impromptu wrapper function wherever we wouldn't inline the wrapper with a W/W approach. ~ There is the option of achieving this without eta-expansion if we instead expand the partial application code to check for demands on the calling convention and for it to evaluate the arguments. The main downsides there would be the complexity of the implementation and that it carries a certain overhead even for functions who don't take advantage of this functionality. I haven't tried this approach because it's not trivial to implement and doing W/W splits seems to work well enough. Currently we use the first approach A) by default, with a flag that allows users to fall back to the more aggressive approach B). I also tried the third approach C) using eta-expansion at call sites to avoid modifying the PAP-handling code which wasn't fruitful. See https://gitlab.haskell.org/ghc/ghc/-/merge_requests/5614#note_389903. We could still try to do C) in the future by having PAP calls which will evaluate the required arguments before calling the partially applied function. But this would be neither a small nor simple change so we stick with A) and a flag for B) for now. See also Note [EPT enforcement] and Note [CBV Function Ids]
References 2
- EPT enforcement GHC.Stg.EnforceEpt
- CBV Function Ids GHC.Types.Id.Info
Referenced by 3
- Worker/wrapper for Strictness and Absence GHC.Core.Opt.WorkWrap.Utils
- GHC.Core.Opt.WorkWrap.Utils call site
- Worker/wrapper for strict arguments GHC.Core.Opt.WorkWrap.Utils