-
Notifications
You must be signed in to change notification settings - Fork 4.8k
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
Allow tail call optimization for call + nop + ret #58622
Comments
Tagging subscribers to this area: @JulieLeeMSFT Issue DetailsAt the moment JIT could optimize tail calls with There is a known problem in F# compiler where it emits Unfortunately @dsyme decided not to fix that issue. My proposal is to add 2 more patterns for JIT to eliminate tail calls: Small repro from issue above which still fails with StackOverflow Exception module Main
type Foo = struct
end
and [<AbstractClass>] Bar() =
abstract bar: foo: byref<Foo> -> unit
let bar =
{ new Bar() with
override x.bar (foo: byref<Foo>) =
x.bar(&foo) } // tail call here
[<EntryPoint>]
let main _ =
let mutable a = new Foo()
bar.bar(&a)
0 Workaround
|
@jakobbotsch I'll take a look if you don't mind since I'm currently inspecting tailcall related sources |
@jakobbotsch I'm seeing an actual nop here for Release as well (in ILDasm) sharplab doesn't render it |
@jakobbotsch F# generates If you remove that |
It's still only guaranteed if it is |
@jakobbotsch I do realize it is UB, but if I'm forced to do ildasm+ilasm fixes, Im totally ok with that :) |
It seems to me that the underlying issue is that F#'s analysis of when to insert tail. prefix is too conservative. It does not insert it here because of the byref parameter, even though one can statically do the analysis to determine that the byref never points locally (the JIT is doing an approximation of this analysis, which is why it is able to do the tailcall as an optimization). Ideally the F# compiler should do a similar analysis that Roslyn does to determine whether byrefs are allowed to escape or not. cc @dotnet/fsharp I'm ok with the fix in #58626, but just note that you are depending on an optimization that the JIT might or might not do depending on any number of factors (platform/architecture, passing large structs as other arguments to the call, using structs fields in the caller function etc.). |
Some notes:
@sver I personally think it's better to take this request through the F# language design process rather than making adhoc changes to the JIT, though the changes aren't wrong in themselves (the JIT can take extra tailcalls if it wants). For example, now byref analysis is stronger in the F# compiler due to the Spans work, I can see the argument for guaranteeing a tailcall in the situation above. However that is a change to the F# language specification and should go through https://github.com/fsharp/fslang-suggestions and https://github.com/fsharp/fslang-design RFC, because we will need a precise specification at the language level. |
@jakobbotsch You read my mind |
I think it's inevitable due to ABI differences and happens even today, e.g. #58522 (comment) |
My understanding is this:
|
So in general, this tail call elimination was present in .NET Framework age even before .NET Core, so this particular change is not something new, but rather make same optimization wider and more consistent. As a neat side effect it fixes some inevitable And I'm fully agree that it should be fixed on language level as well, making F# even more intelligent. I was always under the impression that relying on JIT ASM output is "here will be dragons"-zone of coding and I'm pretty sure that anyone who uses such optimisations or rely on them knows what they are doing. |
That's correct.
Depending on the platform that might be true. The reverse is also true -- we do more tailcalls in .NET core than in .NET framework. But we don't guarantee that optimizations done in .NET framework are also done in .NET core and vice versa. |
My position is this: the spec of when tailcalls are taken is fundamentally part of the programming model, not its implementation. This means that each time "extra" tailcall optimizations are added, expanded or adjusted the programming model changes, and guidance must be rewritten, and programmers re-educated. And changing the tailcall spec should, for example, ideally always be associated with a So from my perspective, changing or expanding when ".NET decides to take a tailcall even when
The nops are by design - F# is deciding not to emit a tailcall so is putting down a nop for debugging after the call return.
Programmers implicitly rely on these characteristics all the time without realising it. |
If I am not mistaken, Roslyn will never emit nops after calls in Release so the change I made in JIT is purely F# only and since Don has a strong (completely understandable) opinion against it I guess I will just close the PR. Thanks for bringing this issue to our attention, it's something we should indeed keep in mind (potential issues with platform-dependent tailcalls). |
Okay, it seems that that adding ad-hoc tailcalls in JIT is not what people want, so I'm closing the issue. |
At the moment JIT could eliminate tail calls with
call+ret
orcallvirt+ret
patterns.There is a known problem in F# compiler where it emits
nop
instruction in betweencall
andret
instructions even inRelease
modedotnet/fsharp#6026
Unfortunately @dsyme decided not to fix that issue.
My proposal is to add 2 more patterns for JIT to eliminate tail calls:
call+nop+ret
andcallvirt+nop+ret
.Small repro from issue above which still fails with StackOverflow Exception
Workaround
The text was updated successfully, but these errors were encountered: