Avoid per-call FSharpFunc closure in remapVal member-info remap - #20426
Avoid per-call FSharpFunc closure in remapVal member-info remap#20426T-Gro wants to merge 1 commit into
Conversation
|
|
Weird. The optimiser should be taught some new tricks:) |
f3c7581 to
520b9bc
Compare
The Option.map mapping in remapValData was a partial application of remapMemberInfo, reified into a per-call FSharpFunc even though Option.map is inline. A syntactic lambda lets [<InlineIfLambda>] fire and lowers it to a direct call, removing one ~72 B closure allocation per copied Val. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 63a36ad7-0e8a-497d-9e96-7d7f03828b9a
520b9bc to
14f7867
Compare
|
Agreed. xs |> Option.map (fun x -> f a b x) // fires → no closure
xs |> Option.map (f a b) // under-applied App(Val) → closure keptPrototyping the general trick — eta-expand under-applied known-arity |
…remove closures When a partial application of a module-level function is passed to an [<InlineIfLambda>] parameter (e.g. `xs |> Option.map (f a b)`), a non-trivial captured argument (a field read, a call) forced a per-call FSharpFunc closure: the optimizer only beta-reduces a lambda-valued argument whose optimization info is a syntactic lambda, and a surviving let for the effectful capture keeps the argument's info UnknownValue. The InlineIfLambda argument binding is now eta-expanded to a lambda with its captured evaluations floated above the binding, so the parameter's uses beta-reduce and the closure is eliminated. Generalizes the single call site hand-fixed in #20426. - Recognition is an EtaFloatableValLet active pattern; the float is a small floatEtaCaptures helper. Reuses TryEtaExpandUnderAppliedValApp (shared with LowerCalls). - Captured arguments are evaluated exactly once, in left-to-right order. - Fires only for [<InlineIfLambda>] parameter bindings, so it never eta-floats a module-level value definition or a partial application inside an SRTP inline-member body, which cross-project measurement showed can multiply closures. No closure-count change in FSharp.Core, FSharpPlus, IcedTasks or the compiler test suite. - Self-compiling FSharp.Compiler.Service.dll: FSharpFunc closure types 16224 -> 16203 (-21). Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 42beb491-2421-4b80-9586-e03454f76524
…remove closures When a partial application of a module-level function is passed to an [<InlineIfLambda>] parameter (e.g. `xs |> Option.map (f a b)`), a non-trivial captured argument (a field read, a call) forced a per-call FSharpFunc closure: the optimizer only beta-reduces a lambda-valued argument whose optimization info is a syntactic lambda, and a surviving let for the effectful capture keeps the argument's info UnknownValue. The InlineIfLambda argument binding is now eta-expanded to a lambda with its captured evaluations floated above the binding, so the parameter's uses beta-reduce and the closure is eliminated. Generalizes the single call site hand-fixed in #20426. - Recognition is an EtaFloatableValLet active pattern; the float is a small floatEtaCaptures helper. Reuses TryEtaExpandUnderAppliedValApp (shared with LowerCalls). - Captured arguments are evaluated exactly once, in left-to-right order. - Fires only for [<InlineIfLambda>] parameter bindings, so it never eta-floats a module-level value definition or a partial application inside an SRTP inline-member body, which cross-project measurement showed can multiply closures. No closure-count change in FSharp.Core, FSharpPlus, IcedTasks or the compiler test suite. - Self-compiling FSharp.Compiler.Service.dll: FSharpFunc closure types 16224 -> 16203 (-21). Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 42beb491-2421-4b80-9586-e03454f76524
|
@kerams : #20487 is the trick for this scenario of immediately passing a partially applied function to a HOF. There are other scenarios worth measuring/watching on top of that still, like a |
…remove closures When a partial application of a module-level function is passed to an [<InlineIfLambda>] parameter (e.g. `xs |> Option.map (f a b)`), a non-trivial captured argument (a field read, a call) forced a per-call FSharpFunc closure: the optimizer only beta-reduces a lambda-valued argument whose optimization info is a syntactic lambda, and a surviving let for the effectful capture keeps the argument's info UnknownValue. The InlineIfLambda argument binding is now eta-expanded to a lambda with its captured evaluations floated above the binding, so the parameter's uses beta-reduce and the closure is eliminated. Generalizes the single call site hand-fixed in #20426. - Recognition is an EtaFloatableValLet active pattern; the float is a small floatEtaCaptures helper. Reuses TryEtaExpandUnderAppliedValApp (shared with LowerCalls). - Captured arguments are evaluated exactly once, in left-to-right order. - Fires only for [<InlineIfLambda>] parameter bindings, so it never eta-floats a module-level value definition or a partial application inside an SRTP inline-member body, which cross-project measurement showed can multiply closures. No closure-count change in FSharp.Core, FSharpPlus, IcedTasks or the compiler test suite. - Self-compiling FSharp.Compiler.Service.dll: FSharpFunc closure types 16224 -> 16203 (-21). Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 42beb491-2421-4b80-9586-e03454f76524
remapValDataruns once per copiedVal(import, signature matching, inlining) and allocated anFSharpFuncfor theOption.mapmapping on every call — even for non-member vals, and even thoughOption.mapisinline. The partial applicationremapMemberInfo ctxt … tmenvwas the allocation; a syntactic lambda lets[<InlineIfLambda>]fire and lowers it to a direct call.remapValDataIL, before → after:newobjper callmemberInfoR@1696closure (72 B)callvirt FSharpFunc::Invokecall remapMemberInfoClosures removed per self-compile (one 72 B
FSharpFuncperremapValDatacall):