[Git][ghc/ghc][wip/apk/unfold-dicts] 3 commits: Tweak inlining discout for unary class ops.
Andreas Klebinger pushed to branch wip/apk/unfold-dicts at Glasgow Haskell Compiler / GHC Commits: 001da929 by Andreas Klebinger at 2026-09-11T20:01:15+02:00 Tweak inlining discout for unary class ops. Consider the number of arguments applied to a unary class op, not just it'S unary-ness when computing class op application size. Fixes #27802 - - - - - 658c5cc9 by Andreas Klebinger at 2026-09-15T06:13:00+00:00 Try discounting args applied to a result of a class op - - - - - 697dddd3 by Andreas Klebinger at 2026-09-15T11:31:11+00:00 Generalize to the unknown call case - - - - - 5 changed files: - compiler/GHC/Core/Unfold.hs - compiler/GHC/Core/Unfold.hs-boot - compiler/GHC/Core/Utils.hs - compiler/GHC/Driver/Session.hs - docs/users_guide/using-optimisation.rst Changes: ===================================== compiler/GHC/Core/Unfold.hs ===================================== @@ -30,6 +30,7 @@ module GHC.Core.Unfold ( updateFunAppDiscount, updateDictDiscount, updateVeryAggressive, updateCaseScaling, updateCaseThreshold, updateReportPrefix, + updateUnknownCallArg, inlineBoringOk, calcUnfoldingGuidance, uncondInlineJoin @@ -87,6 +88,9 @@ data UnfoldingOpts = UnfoldingOpts , unfoldingCaseScaling :: !Int -- ^ Penalize depth with 1/x + , unfoldingUnknownCallArg :: !Int + -- ^ Penalize depth with 1/x + , unfoldingReportPrefix :: !(Maybe String) -- ^ Only report inlining decisions for names with this prefix } @@ -121,6 +125,11 @@ defaultUnfoldingOpts = UnfoldingOpts -- Penalize depth with (size*depth)/scaling , unfoldingCaseScaling = 30 + -- Makes functions more likely to inline if they apply + -- a function argument to interesting arguments. + -- See Note [Discounting for known-at-callsite function calls] + , unfoldingUnknownCallArg = 10 + -- Don't filter inlining decision reports , unfoldingReportPrefix = Nothing } @@ -149,6 +158,11 @@ updateCaseThreshold n opts = opts { unfoldingCaseThreshold = n } updateCaseScaling :: Int -> UnfoldingOpts -> UnfoldingOpts updateCaseScaling n opts = opts { unfoldingCaseScaling = n } +updateUnknownCallArg :: Int -> UnfoldingOpts -> UnfoldingOpts +updateUnknownCallArg n opts = opts { unfoldingUnknownCallArg = n } + + + updateReportPrefix :: Maybe String -> UnfoldingOpts -> UnfoldingOpts updateReportPrefix n opts = opts { unfoldingReportPrefix = n } @@ -335,6 +349,8 @@ isValFun :: CoreExpr -> Bool -- one top-level value lambda isValFun (Lam b e) | isRuntimeVar b = True | otherwise = isValFun e +isValFun (Cast e _c) = isValFun e +isValFun (Tick _t e) = isValFun e isValFun _ = False calcUnfoldingGuidance @@ -709,10 +725,10 @@ sizeExpr opts !bOMB_OUT_SIZE top_args expr FCallId _ -> sizeN (callSize (length val_args) voids) DataConWorkId dc -> conSize dc (length val_args) PrimOpId op _ -> primOpSize op (length val_args) - ClassOpId cls _ -> classOpSize opts cls top_args val_args + ClassOpId cls _ -> classOpSize opts cls top_args val_args voids _ | fun `hasKey` buildIdKey -> buildSize | fun `hasKey` augmentIdKey -> augmentSize - | otherwise -> funSize opts top_args fun (length val_args) voids + | otherwise -> funSize opts top_args fun val_args voids ------------ size_up_alt (Alt _con _bndrs rhs) = size_up rhs `addSizeN` 10 @@ -777,21 +793,29 @@ litSize _other = 0 -- Must match size of nullary constructors -- Key point: if x |-> 4, then x must inline unconditionally -- (eg via case binding) -classOpSize :: UnfoldingOpts -> Class -> [Id] -> [CoreExpr] -> ExprSize +classOpSize :: UnfoldingOpts -> Class -> [Id] -> [CoreExpr] -> Int -> ExprSize -- See (IA1) in Note [Interesting arguments] in GHC.Core.Opt.Simplify.Utils -classOpSize _opts _cls _top_args [] +classOpSize _opts _cls _top_args [] _voids = sizeZero -- A non-applied classop -classOpSize opts cls top_args (dict_arg:other_val_args) - = SizeIs size (arg_discount dict_arg) 0 +classOpSize opts cls top_args (dict_arg:other_val_args) voids + = SizeIs size dict_arg_discount 0 where - size | isUnaryClass cls = 0 -- See (UCM4) in Note [Unary class magic] in GHC.Core.TyCon - | otherwise = 20 + (10 * length other_val_args) + -- See (UCM4) in Note [Unary class magic] in GHC.Core.TyCon + op_app_size = if isUnaryClass cls then 0 else 20 + + -- Size penalty for applying the extracted class method to it's + -- arguments. + method_app_size = callSize (length other_val_args) voids + + size = op_app_size + method_app_size -- If the class op is scrutinising a lambda bound dictionary then -- give it a discount, to encourage the inlining of this function - arg_discount (Cast arg _co) = arg_discount arg - arg_discount (Var dict) | dict `elem` top_args = unitBag (dict, dict_discount) - arg_discount _ = emptyBag + dict_arg_discount = case getIdFromTrivialExpr_maybe dict_arg of + Nothing -> emptyBag + Just dict + | dict `elem` top_args -> unitBag (dict, dict_discount) + | otherwise -> emptyBag -- If we have (class-op d arg1 .. argn) then it's super-good to inline -- to expose `d`; not only can we do the dictionary selection @@ -800,7 +824,9 @@ classOpSize opts cls top_args (dict_arg:other_val_args) -- See the discussion on #26831, esp "Delicate inlining". dict_discount | null other_val_args = unfoldingDictDiscount opts - | otherwise = unfoldingDictDiscount opts + unfoldingFunAppDiscount opts + | otherwise = unfoldingDictDiscount opts + unfoldingFunAppDiscount opts + + -- See Note [Discounting for known-at-callsite function calls] + unknownFunArgDiscount opts top_args other_val_args -- | The size of a function call callSize @@ -826,12 +852,13 @@ jumpSize _n_val_args _voids = 0 -- Jumps are small, and we don't want penalise -- spectral/puzzle. TODO Perhaps adjusting the default threshold would be a -- better solution? -funSize :: UnfoldingOpts -> [Id] -> Id -> Int -> Int -> ExprSize +funSize :: UnfoldingOpts -> [Id] -> Id -> [CoreExpr] -> Int -> ExprSize -- Size for function calls where the function is not a constructor or primops -- Note [Function applications] -funSize opts top_args fun n_val_args voids +funSize opts top_args fun val_args voids | otherwise = SizeIs size arg_discount res_discount where + n_val_args = length val_args some_val_args = n_val_args > 0 is_join = isJoinId fun @@ -839,10 +866,14 @@ funSize opts top_args fun n_val_args voids | not some_val_args = 0 | otherwise = callSize n_val_args voids + fun_discount + | fun `elem` top_args = unfoldingFunAppDiscount opts + unknownFunArgDiscount opts top_args val_args + | otherwise = unfoldingFunAppDiscount opts + -- DISCOUNTS -- See Note [Function and non-function discounts] arg_discount | some_val_args && fun `elem` top_args - = unitBag (fun, unfoldingFunAppDiscount opts) + = unitBag (fun, fun_discount) | otherwise = emptyBag -- If the function is an argument and is applied -- to some values, give it an arg-discount @@ -864,7 +895,64 @@ conSize dc n_val_args -- See Note [Constructor size and result discount] | otherwise = SizeIs 10 emptyBag 10 -{- Note [Constructor size and result discount] +-- See Note [Discounting for known-at-callsite function calls] +unknownFunArgDiscount :: UnfoldingOpts -> [Id] -> [CoreExpr] -> Int +unknownFunArgDiscount opts top_args args = sum (map arg_discount args) + where + arg_discount arg + | interestingArg arg = unfoldingUnknownCallArg opts + | otherwise = 0 + + -- Little brother to the simplifier's interestingArg. + -- Use trivial_expr_fold to look through casts, ticks and type applications, + -- in a principled manner. + interestingVar v = v `elem` top_args || exprIsConLike (Var v) + interestingArg = trivial_expr_fold interestingVar (const True) False True + +{- +Note [Discounting for known-at-callsite function calls] +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +A regular unknown function call `f x y` inside some function `foo` provides no +optimization opportunities to improve `foo` no matter what shape x and y have. + +However we might have a pattern where a function argument is applied to +interesting arguments like: + + foo f g x = ... f x ... g g ... + bar = foo snd snd (1,2) + +Here we really want to inline `foo`, as it will expose `snd` to the tuple, causing +it to inline and eliminating the tuple allocation completely. + +Note that this doesn't always require the function argument itself to be inlined. +Inlining `foo` will also expose the strictness and W/W properties of foos function +arguments and allows specConstr to fire on them. There is simple a large number of +optimizations that can happen for known functions that are impossible for unknown +calls. + +So here is the plan: Whenever the argument to `f` could be useful for optimizing +`foo` if `f` is a known call we give `f` a discount. + +This includes: + +* The argument is a top_arg itself. (Think `f id 1` with `f g = g`) +* The argument is con like: This can help rules, W/W, SpecConstr, further inlining. +* The argument is a literal: Rules/Inlining +* The argument is non-trivial: + + If it's a thunk strictness might allow eager evaluationg + + If it's a pap/lambda we might be able to eta-expand it, removing an intermediate pap + or even removing the PAP fully. + +However we have to be careful. It's *not* a given that we actually +get a benefit from inlining such a call into it's context. It fully +depends on the specific arguments. So we simply make those interesting +arguments "free" rather charging the usual cost of 10 per applied arg. + +Similarly we *only* should give this discount to a unknown call. If the function +`f` being called inside `foo` is a known function all those optimizations can +happen inside foo without inlining it into it's call sites. + +Note [Constructor size and result discount] ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Treat a constructors application as size 10, regardless of how many arguments it has; we are keen to expose them (and we charge separately ===================================== compiler/GHC/Core/Unfold.hs-boot ===================================== @@ -13,3 +13,4 @@ updateDictDiscount :: Int -> UnfoldingOpts -> UnfoldingOpts updateVeryAggressive :: Bool -> UnfoldingOpts -> UnfoldingOpts updateCaseThreshold :: Int -> UnfoldingOpts -> UnfoldingOpts updateCaseScaling :: Int -> UnfoldingOpts -> UnfoldingOpts +updateUnknownCallArg :: Int -> UnfoldingOpts -> UnfoldingOpts ===================================== compiler/GHC/Core/Utils.hs ===================================== @@ -1654,7 +1654,9 @@ it off at source. {-# INLINE trivial_expr_fold #-} trivial_expr_fold :: (Id -> r) -> (Literal -> r) -> r -> r -> CoreExpr -> r --- ^ The worker function for Note [exprIsTrivial] and Note [getIdFromTrivialExpr] +-- ^ k_id k_lit k_triv k_not_triv +-- +-- The worker function for Note [exprIsTrivial] and Note [getIdFromTrivialExpr] -- This is meant to have the code of both functions in one place and make it -- easy to derive custom predicates. -- ===================================== compiler/GHC/Driver/Session.hs ===================================== @@ -1881,6 +1881,8 @@ dynamic_flags_deps = [ (intSuffix (\n d -> d { unfoldingOpts = updateCaseThreshold n (unfoldingOpts d)})) , make_ord_flag defFlag "funfolding-case-scaling" (intSuffix (\n d -> d { unfoldingOpts = updateCaseScaling n (unfoldingOpts d)})) + , make_ord_flag defFlag "funfolding-unknown-call-arg-discount" + (intSuffix (\n d -> d { unfoldingOpts = updateUnknownCallArg n (unfoldingOpts d)})) , make_dep_flag defFlag "funfolding-keeness-factor" (floatSuffix (\_ d -> d)) ===================================== docs/users_guide/using-optimisation.rst ===================================== @@ -1902,6 +1902,26 @@ as such you shouldn't need to set any of them explicitly. A flag recommended. Values in the range 10 <= n <= 20 allow some inlining to take place while still allowing GHC to compile modules containing such inlining loops. +.. ghc-flag:: -funfolding-unknown-call-arg-discount=⟨n⟩ + :shortdesc: *default: 10.* Discount for each interesting arg applied to a unknown function. + :type: dynamic + :category: + + :default: 10 + + .. index:: + single: inlining, controlling + single: unfolding, controlling + + Typically a function like ``foo f x`` is not much more likely to inline if + ``f`` is a known argument. + + With this discount being given we make GHC more eager to inline ``foo`` + if ``f`` is applied to interesting arguments inside the RHS of ``foo``. + + This optimizes cases where other optimizations can fire on the call to ``f`` + after it has been made a known call by inlining ``foo`` into it's call site. + .. ghc-flag:: -fworker-wrapper :shortdesc: Enable the worker/wrapper transformation. Implied by :ghc-flag:`-O` View it on GitLab: https://gitlab.haskell.org/ghc/ghc/-/compare/3520030fa377abd59eaca4a61d16f38... -- View it on GitLab: https://gitlab.haskell.org/ghc/ghc/-/compare/3520030fa377abd59eaca4a61d16f38... You're receiving this email because of your account on gitlab.haskell.org. Manage all notifications: https://gitlab.haskell.org/-/profile/notifications | Help: https://gitlab.haskell.org/help
participants (1)
-
Andreas Klebinger (@AndreasK)