Uh oh!
There was an error while loading. Please reload this page.
Cancellable: built-in stack guard - #18285
Conversation
❗ Release notes requiredCaution No release notes found for the changed paths (see table below). Please make sure to add an entry with an informative description of the change as well as link to this pull request, issue and language suggestion if applicable. Release notes for this repository are based on Keep A Changelog format. The following format is recommended for this repository:
If you believe that release notes are not necessary for this PR, please add NO_RELEASE_NOTES label to the pull request. You can open this PR in browser to add release notes: open in github.dev
|
vzarytovskii
commented
Jan 29, 2025
What's the codegen for calling cancellable from itself with let!, return!? Stackguard will have to wrap the cancellable in this case (either properly inline or be part of the method). |
vzarytovskii
commented
Jan 29, 2025
Also, if it's universally in the cancellable, it might be quite an overhead. I doubt that that closure will be just inlined. |
majocha
commented
Jan 29, 2025
Yes, good questions. For now, it just passes around the depth as int. Maybe it would be possible to branch out on it into fast inlined |
majocha
commented
Jan 30, 2025
Ok, I did some tests on the side to make sure this prevents stackoverflow both in mutually recursive and self-recursive computations, benchmarking with FCSSourceFiles does not show any performance degradation. But! I have a feeling this is not a correct approach, and a trampoline should be used instead, especially given the mutual recursion requirement. |
vzarytovskii
commented
Jan 30, 2025
Out of curiosity, how did you test it? Because it will likely be fine on the CoreCLR, since JIT does a good job on the tail-prefixed calls. Full CLR is where it was causing problems. |
majocha
commented
Jan 30, 2025
I just copied the whole let rectest x =
cancellable {if x >=400_000then
printfn "done!"return0elselet!x= test (x +1)return x
}let recmut1 x =
cancellable {if x >=500_000then
printfn "done!"return0elselet!x= mut2 (x +1)return x
}andmut2 x =
cancellable {let!x= mut1 (x +1)return x
}And made sure it fails before and succeeds with new code. |
majocha
commented
Jan 30, 2025
I think the code in this PR works in practice for some limited depth of recursion, but is incorrect generally. The threads are started before the previous one finishes. If I put a million recursive calls in a test, the whole thing slows down to a crawl. |
majocha
commented
Feb 1, 2025
I've been thinking a little bit, so I just write it down here. fsharp/src/Compiler/Checking/CheckDeclarations.fs Line 5479 in 58560f8 This didn't fully fix things, because there was also some non-tailcall recursion. That's why GuardCancellable was added, e.g.:fsharp/src/Compiler/Checking/CheckDeclarations.fs Lines 5351 to 5352 in 58560f8 It seems this all can be handled more generally using trampoline. For example, async builder does this on every bind: fsharp/src/FSharp.Core/async.fs Lines 247 to 253 in 58560f8 Maybe resumable code would simplify the implementation: coroutine with tailcalls example. I don't think I have the skills, but reimplementing |
Experimentally moved stack guard functionality into
Cancellable.runto make recursion incancellablegenerally safe.If this works, it would allow to simplify some code in CheckDeclarations.fs.