-
Notifications
You must be signed in to change notification settings - Fork 825
Optimize 'string', remove boxing where possible, prevent FS0670, simplify generic code (RFC-1089) #9549
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
Conversation
That's unexpected! What are these tests? Doesn't sound like they rely on |
It also throws this:
Which in turn appears to be a bug in the runtime, or at least a resurfacing of a bug, possibly related are:
Reading all these reports, this warrants some research/debugging etc. It looks like it's caused by something on the stack being null or unknown somehow. |
Ok, that last error comes from the temp
If I load that assembly in ILSpy, it says:
The generated assembly: hkch4czt.2ka.zip /cc @KevinRansom, help? Looks like I need to do "something" when I change the signature of a public function, in this case Here's what I think happens:
Is this how these tests are supposed to work? In particular, the last step, is it possible that it doesn't use the newly compiled |
Those parts of the FCS tests are horribly annoying and not very simple to fix. The FCS tests also don't use the built FSharp.Core since FCS is currently designed to be compatible with dowlevel FSharp.Core binaries. I personally don't like that behavior, but it is what it is. |
Thanks, @cartermp, I just noticed that the actual tests that were failing here had zero dependency on FSharp.Core. Since they used The other ones, from this comment above (#9549 (comment)), I've no idea yet, as I don't see them locally (or I'm doing something wrong). I'm now running all tests locally again, see if I missed something. |
@KevinRansom, in our discussion on this, you asked if we could improve, esp. to prevent excessive boxing. My take on this at the time was: no, we can't combine statically-resolvable type compiler syntax and at the same be non-inline (to prevent FS0670 from throwing). But after some experimenting, and searching for uses in the source code of the Since this idea makes this change larger than originally intended, I decided to go the "long" route and write out an RFC (see fsharp/fslang-design#479) and the corresponding lang suggestion (see fsharp/fslang-suggestions#890), which basically backs the findings of several bug reports. It's a minimal change in the surface area, but a change after all, hence the RFC. So where does that leave us in this PR? Well, in short, we now have the best of both worlds:
All in all, I'm pretty happy with the result in terms of generated IL. Maybe @teo-tsirpanis could also have a second-opinion look at this, as he reported the "bad IL" issue (#9153). Note: the baseline tests will still fail, but I do not want to start working on these unless we're all confident this is the way to go. |
I am still worried by the excessive use of the The core of the issue is that My opinion would be to loosen the aforementioned restriction and in the meantime, use the As for the changes at hand, they seem better than the previous implementation of the I am wondering what makes the enums such a special case. I wrote the following C# program using System;
namespace ToStringTest
{
static class Program
{
static string AnyToString<T>(T x) => x.ToString();
static void Main()
{
Console.WriteLine(AnyToString(521));
Console.WriteLine(AnyToString(ConsoleKey.U));
}
}
} and the results are the expected ones ( |
@teo-tsirpanis, thanks for looking into this. It compiles with The problem in F# is that when you use static type checks, any C#, since 7.3, can distinguish between enum and integral types. That's a fairly recent feature. There's not yet a similar proposal for F#, and I don't know how hard it would be to implement (looking at the C# discussions, it wasn't trivial). What I haven't tried, though, is if calling I'll think about it some more. Btw, it's more than just the null check, that's gone. It's also call vs callvirt, and the second null check (there's no check at all anymore). Casting without boxing is not possible, and calling Note that in the original, none of these special cases were hit, ever. For instance, using it on a decimal, is now equal to doing |
I'll also run some benchmarks. It was already faster, but should be even better now, even for integral types (the ones that are still boxed). The others are as fast as they would be if you'd code it by hand. |
@teo-tsirpanis, maybe there's a solution after all, even for the last cases that still need boxing now. I was stuck on the idea that we must use If we look at the implementation of I think that, if the recent rewrites of all The idea is to change the 8 integral types to look like this, which compiles, and correctly prints the names of enums: when 'T : int32 = (# "" value : 'T #).ToString() I'll do some more testing and create benchmarks for comparison. At least in the past this was much slower than using |
@teo-tsirpanis, I've given this a little bit more thought. Your C# example works locally by using Consider this: Then click Apply and reset your FSI. This will give: > (-12).ToString();;
val it : string = "_12"
> string -12;;
val it : string = "-12" Or with "over line", which apparently is used in some educational systems, so it isn't entirely impossible that this happens in the wild: > (-12).ToString();;
val it : string = "‾12" Conclusion: as much as I would like to use As far as I know it is not possible to convert to Result of all this research, I have to Here's how the current approach looks if it were coded in C# for return ((IFormattable) ((uint) x)).ToString(null, CultureInfo.InvariantCulture); Or if it was an enum: return ((IFormattable) x).ToString(null, CultureInfo.InvariantCulture); EDIT: I've added a language request to allow such syntax. If I knew where in the code this is, I might implement it. Until then, I think we'll have to live with the 90% rule: almost everything is optimized now, the remaining stuff will have to be postponed. See #9594. |
@KevinRansom, I think this is now "as good as it gets". I've updated the original text of the PR and updated the performance charts (as you requested). It's certainly much better now than my first attempt: it's faster, less allocations and better IL code optimizations. If you think this is good enough to get in, I'll set to work on the baseline tests (or maybe you can help there?). |
@abelbraaksma Why not continute to
I believe it is the removal of SRTP that solves this problem for you, rather than the removal of Could you try with the Thanks |
Ping @TIHan, @dsyme. can either or both please review? As mentioned above, I think this is now "as good as it can get" with current features of the language, and improves performance of For the signed-int case, which needs a distinction between |
@abelbraaksma currently we're in a very hard push to finish off and review open type declarations and string interpolation. We're trying to get those completed by the end of this week or maybe the end of next week. After that we should be more "free" :) |
@cartermp Cool, thanks for the update! If I can help in any way with those tasks (tests or otherwise), ping me :) |
If you're up for it, our biggest concern right now is ensuring that tooling works well with string interpolation. If you're able to pull down that branch, build a new VS, and use F# scripts to play with the feature and give feedback then that would be great. Unfortunately it's tricky to use the feature with a real codebase because it relies on preview FSharp.Core functionality that is only accessible in F# scripts or some trickery where a project has its FSharp.Core reference swapped out for the built FSharp.Core. But if you use F# scripts it's fine, which I've been doing. |
@cartermp Yeah, I know how to cheat a different Core reference (used it in benchmarking for before/after in one run), and how to switch the fsc.exe, I'll give it a shot with some scripts and projects I have. |
I came to review this but there are a lot of unrelated changes in the diff in github Anyone know what's up? |
@dsyme It may have something to do with introduction of Most certainly, last time I checked this, it was just a handful of changes. |
@dsyme, nothing a little merge couldn't fix. There are now just 5 files changes, instead of the 67 you saw. Strange, but I do think it was caused by |
Thanks! :-) |
PS: I'm live on Slack if you need/want to discuss a few things. |
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.
This is a good change. I like the additional test coverage, and the results in benchmarking also speak for themselves. Great work!
I would still like @TIHan to take a look here, as this did spark a lengthy conversation about these kinds of optimizations and the things we can and cannot do. |
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.
This is good; we discussed this before.
It does improve things, but it's not the perfect solution - perfect solution requires metadata format changes which I don't think we are willing to accept at the moment; therefore, we can take in this change.
…lify generic code (RFC-1089) (dotnet#9549) * Merge from Re-enable-tests-for-operators * Add tests that cover the changes for fixing FS0670 * Un-inline `string` to allow used in generic overrides and types * Fix 3x C# default impl tests by removing dep. on FSharp.Core.dll * Implement new 'string' ideas from RFC-1089 * Some housekeeping, adding IFormattable to the list * Further optimizations for unsigned ints and (u)nativeint * Adding back 'inline' but not SRTP * Ignore NCrunch temp files and local user files * Fix string of enum-of-char * Fix tests in ExprTests.fs * Distinguish between FSharp.Core 4.5, 4.6 and 4.7 in tests in ExprTests.fs
Fixes #7958
Fixes #9153
Depends on: #9622edit 7/21, now merged.Tentatively implements: https://github.com/fsharp/fslang-design/blob/master/FSharp-5.0/FS-1089-allow-string-everywhere.md
TLDR: after this change,
string
FS0670 will not be raised anymore, while code is still inlined using compiler optimization syntax. Speed is much improved and generated IL and disassembly much smaller. Less boxing and less memory pressure.Before this change:
string
led to exactly the same inlined code, as found by @teo-tsirpanis in The 'string' operator produces suboptimal code when the object's type is known. #9153.string
string X
ifX
was of a generic type specified on the class/record/struct/du, raises FS0670, reported by me in Usingstring obj
vs obj.ToString() leads to problems: the type parameter can escape its scope #7958.ToString()
directlyAfter this change
string
typescall
instead ofcallvirt
for types likefloat
,float32
,decimal
(no boxing)callvirt
withconstrained
forchar
andbool
(no boxing) and the integral unsigned types (4 integers)Uses boxing withcallvirt
for the integral types (8 integers) because of backward compat issue with enumscallvirt
for the integral signed types (4 integers), this is essentially the old behavior.ToString()
for a few known sealed typesCode generation example
When having
let fromFloat (x: float) = string x
, before it looked like this:After this change, it's this:
Performance effects
Important note, likely the timings will be better, as this was with the new version using debug jit, vs original using optimized jit, see BDN bug dotnet/BenchmarkDotNet#1493.
EDIT: New charts, as the change has now evolved and it is much better
Across the board: increase in speed up to 10x, typically around 20-30%. Less allocations, about 40% on average where it applies, sometimes down to zero.
Performance of numeric types
A note on numeric types: the unsigned types use
ToString()
, which is faster and works correctly withEnum
. The signed types useIFormattable::ToString(null, InvariantCulture)
, because there's the off-chance that the minus sign is defined in "current culture", and we cannot use that. Hence, the speed increase is larger for the unsigned types.Other numeric types use the fastest path possible, which means a
constrained callvirt
onToString()
or acall
on the proper variant ofIFormattable::ToString
.Performance of char, bool, enum
The numbers speak for themselves. Note the 10x improvement for
bool
and the fact that there are now zero allocations in that case:Performance of other types
Below the improvement of
string
(gets erased, so 0.000ns!),Guid
,DateTime
a simple custom DU with a ToString override, and a type that is already cast toIFormattable
.