-
Notifications
You must be signed in to change notification settings - Fork 875
Eliminate per-call closure for InlineIfLambda partial applications #20487
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
T-Gro
wants to merge
1
commit into
main
Choose a base branch
from
t-gro-optimizer-etaexpand-inlineiflambda-spike
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
55 changes: 55 additions & 0 deletions
55
tests/FSharp.Compiler.ComponentTests/EmittedIL/InlineIfLambdaEtaFloat.fs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,55 @@ | ||
| // Copyright (c) Microsoft Corporation. All Rights Reserved. See License.txt in the project root for license information. | ||
|
|
||
| namespace EmittedIL | ||
|
|
||
| open Xunit | ||
| open FSharp.Test.Compiler | ||
|
|
||
| module InlineIfLambdaEtaFloat = | ||
|
|
||
| // No-closure facts assert the closure *type* is absent; the negative controls assert it is invoked. | ||
| let private intIntFunc = "FSharpFunc`2<int32,int32>" | ||
| let private intIntInvoke = intIntFunc + "::Invoke" | ||
|
|
||
| let private compileOpt source = | ||
| FSharp source |> withOptimize |> asLibrary |> compile |> shouldSucceed | ||
|
|
||
| [<Fact>] | ||
| let ``Module-level function capture allocates no closure`` () = | ||
| compileOpt """ | ||
| module Test | ||
| type Box(v: int) = member _.Value = v | ||
| let f4 (a:int) (b:int) (c:int) (x:int) = a + b + c + x | ||
| let shapeB (b: Box) (o: int option) = o |> Option.map (f4 1 b.Value 3) | ||
| """ | ||
| |> verifyILNotPresent [ intIntFunc ] | ||
|
|
||
| [<Fact>] | ||
| let ``Generic module-level function capture allocates no closure`` () = | ||
| compileOpt """ | ||
| module Test | ||
| type Box(v: int) = member _.Value = v | ||
| let gpick (a:'T) (b:'T) (c:'T) (x:'T) : 'T = a | ||
| let shapeG (b: Box) (o: int option) = o |> Option.map (gpick 1 b.Value 3) | ||
| """ | ||
| |> verifyILNotPresent [ intIntFunc ] | ||
|
|
||
| // A first-class function argument has no known arity, so there is nothing to eta-expand. | ||
| [<Fact>] | ||
| let ``First-class function argument keeps its closure`` () = | ||
| compileOpt """ | ||
| module Test | ||
| let shapeD (g: int -> int) (o: int option) = o |> Option.map g | ||
| """ | ||
| |> verifyILPresent [ intIntInvoke ] | ||
|
|
||
| // A static member lacks the module-level known-arity shape the transform keys on. | ||
| [<Fact>] | ||
| let ``Static-member callee keeps its closure`` () = | ||
| compileOpt """ | ||
| module Test | ||
| type Box(v: int) = member _.Value = v | ||
| type H = static member SF (a:int) (b:int) (c:int) (x:int) = a + b + c + x | ||
| let shapeM (b: Box) (o: int option) = o |> Option.map (H.SF 1 b.Value 3) | ||
| """ | ||
| |> verifyILPresent [ intIntInvoke ] |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
129 changes: 129 additions & 0 deletions
129
tests/FSharp.Compiler.ComponentTests/Optimizations/InlineIfLambdaEtaFloat.fs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,129 @@ | ||
| // Copyright (c) Microsoft Corporation. All Rights Reserved. See License.txt in the project root for license information. | ||
|
|
||
| namespace Optimizations | ||
|
|
||
| open Xunit | ||
| open FSharp.Test.Compiler | ||
|
|
||
| module InlineIfLambdaEtaFloat = | ||
|
|
||
| // Each snippet runs under both --optimize+ (transform on) and --optimize- (off, as a reference oracle), | ||
| // in-process, so a failure signals with failwith, not exit. | ||
| let private run (optimize: bool) (source: string) = | ||
| Fsx source |> withOptimization optimize |> compileExeAndRun |> shouldSucceed |> ignore | ||
|
|
||
| [<Theory; InlineData(true); InlineData(false)>] | ||
| let ``Captured argument is read once, before the parameter body is copied`` (optimize: bool) = | ||
| run optimize """ | ||
| let mutable state = 10 | ||
| let f4 (a:int) (b:int) (c:int) (x:int) = a + b + c + x | ||
| let inline twiceMut ([<InlineIfLambda>] f: int -> int) (x: int) = | ||
| let r1 = f x | ||
| state <- 99 | ||
| let r2 = f x | ||
| r1 + r2 | ||
| if twiceMut (f4 1 state 3) 5 <> 38 then failwith "capture was not read exactly once at 10" | ||
| """ | ||
|
|
||
| [<Theory; InlineData(true); InlineData(false)>] | ||
| let ``Captured effect runs exactly once for a multi-use parameter`` (optimize: bool) = | ||
| run optimize """ | ||
| let mutable reads = 0 | ||
| type Box() = member _.Value = reads <- reads + 1; 10 | ||
| let f4 (a:int) (b:int) (c:int) (x:int) = a + b + c + x | ||
| let inline twice ([<InlineIfLambda>] f: int -> int) (x: int) = f x + f x | ||
| let r = twice (f4 1 (Box().Value) 3) 5 | ||
| if r <> 38 then failwithf "Expected 38 but got %d" r | ||
| if reads <> 1 then failwithf "Captured getter evaluated %d times, expected 1" reads | ||
| """ | ||
|
|
||
| // Floating must not sink the capture into the used branch. | ||
| [<Theory; InlineData(true); InlineData(false)>] | ||
| let ``Captured effect runs even when the parameter is unused`` (optimize: bool) = | ||
| run optimize """ | ||
| let mutable eff = 0 | ||
| type Box() = member _.Value = eff <- eff + 1; 10 | ||
| let f4 (a:int) (b:int) (c:int) (x:int) = a + b + c + x | ||
| None |> Option.map (f4 1 (Box().Value) 3) |> ignore | ||
| Some 5 |> Option.map (f4 1 (Box().Value) 3) |> ignore | ||
| if eff <> 2 then failwithf "Expected 2 captures but got %d" eff | ||
| """ | ||
|
|
||
| [<Theory; InlineData(true); InlineData(false)>] | ||
| let ``Captured arguments preserve left-to-right evaluation order`` (optimize: bool) = | ||
| run optimize """ | ||
| let log = System.Collections.Generic.List<string>() | ||
| let tap (name: string) (v: int) = log.Add name; v | ||
| let f4 (a:int) (b:int) (c:int) (x:int) = a + b + c + x | ||
| Some 5 |> Option.map (f4 (tap "a" 1) (tap "b" 2) (tap "c" 3)) |> ignore | ||
| if String.concat "," (List.ofSeq log) <> "a,b,c" then failwithf "Wrong order: %A" (List.ofSeq log) | ||
| """ | ||
|
|
||
| // The transform re-applies to the second parameter's binding, nested under the first. | ||
| [<Theory; InlineData(true); InlineData(false)>] | ||
| let ``Two InlineIfLambda parameters each capture once, left-to-right`` (optimize: bool) = | ||
| run optimize """ | ||
| let log = System.Collections.Generic.List<string>() | ||
| let eff (n: string) (v: int) = log.Add n; v | ||
| let f4 (a:int) (b:int) (c:int) (x:int) = a + b + c + x | ||
| let inline combine ([<InlineIfLambda>] f: int -> int) ([<InlineIfLambda>] g: int -> int) (x: int) = | ||
| f x + g x + f x + g x | ||
| let r = combine (f4 1 (eff "f" 10) 3) (f4 2 (eff "g" 20) 4) 5 | ||
| if r <> 100 then failwithf "Expected 100 but got %d" r | ||
| if String.concat "," (List.ofSeq log) <> "f,g" then failwithf "Wrong capture order/count: %A" (List.ofSeq log) | ||
| """ | ||
|
|
||
| // The "body" marker stays out of the log, proving the throw happens during capture, before the body. | ||
| [<Theory; InlineData(true); InlineData(false)>] | ||
| let ``A throwing capture is raised eagerly, in order, before the body`` (optimize: bool) = | ||
| run optimize """ | ||
| let log = System.Collections.Generic.List<string>() | ||
| let tapOk (name: string) (v: int) = log.Add name; v | ||
| let tapThrow (name: string) : int = log.Add name; failwith ("throw-" + name) | ||
| let f4 (a:int) (b:int) (c:int) (x:int) = a + b + c + x | ||
| let inline twice ([<InlineIfLambda>] f: int -> int) (x: int) = log.Add "body"; f x + f x | ||
| let mutable msg = "NOTHROW" | ||
| (try twice (f4 (tapOk "a" 1) (tapThrow "b") (tapOk "c" 3)) 5 |> ignore | ||
| with e -> msg <- e.Message) | ||
| if String.concat "," (List.ofSeq log) <> "a,b" then failwithf "Wrong pre-throw order: %A" (List.ofSeq log) | ||
| if msg <> "throw-b" then failwithf "Wrong exception surfaced: %s" msg | ||
| """ | ||
|
|
||
| [<Theory; InlineData(true); InlineData(false)>] | ||
| let ``Capture is evaluated once when the parameter escapes in a returned closure`` (optimize: bool) = | ||
| run optimize """ | ||
| let mutable reads = 0 | ||
| type Box() = member _.Value = reads <- reads + 1; 10 | ||
| let f4 (a:int) (b:int) (c:int) (x:int) = a + b + c + x | ||
| let inline makeAdder ([<InlineIfLambda>] f: int -> int) = fun y -> f y + f y | ||
| let g = makeAdder (f4 1 (Box().Value) 3) | ||
| let r1 = g 5 | ||
| let r2 = g 6 | ||
| if r1 <> 38 || r2 <> 40 then failwithf "Expected 38/40 but got %d/%d" r1 r2 | ||
| if reads <> 1 then failwithf "Captured getter evaluated %d times, expected 1" reads | ||
| """ | ||
|
|
||
| [<Theory; InlineData(true); InlineData(false)>] | ||
| let ``A compound-expression capture is evaluated once`` (optimize: bool) = | ||
| run optimize """ | ||
| let mutable reads = 0 | ||
| let bump () = reads <- reads + 1; reads | ||
| let f4 (a:int) (b:int) (c:int) (x:int) = a + b + c + x | ||
| let inline twice ([<InlineIfLambda>] f: int -> int) (x: int) = f x + f x | ||
| let r = twice (f4 1 (let n = bump () in n * 10 + 5) 3) 5 | ||
| if r <> 48 then failwithf "Expected 48 but got %d" r | ||
| if reads <> 1 then failwithf "Compound capture evaluated %d times, expected 1" reads | ||
| """ | ||
|
|
||
| // The transform does not fire here: the inline SRTP body collapses first. | ||
| [<Theory; InlineData(true); InlineData(false)>] | ||
| let ``SRTP partial application stays correct`` (optimize: bool) = | ||
| run optimize """ | ||
| let mutable reads = 0 | ||
| type Box() = member _.Value = reads <- reads + 1; 10 | ||
| let inline addThree (a: ^T) (b: ^T) (c: ^T) (x: ^T) = a + b + c + x | ||
| let inline mapTwice ([<InlineIfLambda>] f: ^U -> ^U) (x: ^U) = f (f x) | ||
| let r = mapTwice (addThree 1 (Box()).Value 3) 5 | ||
| if r <> 33 then failwithf "Expected 33 but got %d" r | ||
| if reads <> 1 then failwithf "Captured getter evaluated %d times, expected 1" reads | ||
| """ |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Partial applications of inline/SRTP functions and curried members can still allocate closures - should be reflected in the release note?