-
Notifications
You must be signed in to change notification settings - Fork 797
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
The 'string' operator produces suboptimal code when the object's type is known. #9153
Comments
Why not the simpler approach: to compile If If |
@charlesroddie it might be possible that a subclass of |
If This would not make |
@charlesroddie, your proposal is pretty much what @dsyme and I researched a few months ago, see #7958 (I forgot about creating the pr, this thread reminded me again). Though the reasons to want to change |
Existing code from #7958 (comment) let inline anyToString nullStr x =
match box x with
| null -> nullStr
| :? System.IFormattable as f -> f.ToString(null,System.Globalization.CultureInfo.InvariantCulture)
| obj -> obj.ToString()
let inline string (value: ^T) =
anyToString "" value
// since we have static optimization conditionals for ints below, we need to special-case Enums.
// This way we'll print their symbolic value, as opposed to their integral one (Eg., "A", rather than "1")
when ^T struct = anyToString "" value
...
when ^T : int32 = (# "" value : int32 #).ToString("g",CultureInfo.InvariantCulture)
... I don't understand the compiler-specific syntax. But the intention seems to be to use the code from lowest down case as the most specific case. So Your suggestion @abelbraaksma removes the static attempts completely. That would entrench the problem in this issue, wouldn't it? I think using anyToString at any time is an inefficiency so we should eliminate use of this function if possible. I don't know if something like this is possible: let inline string (value: ^T) =
value.ToString()
when ^T struct = anyToString "" value
when ^T : null = match (# "" value : ^T #) with | null -> "" | NonNull v -> v.ToString()
when ^T :> IFormattable = (# "" value : IFormattable #).ToString(CultureInfo.InvariantCulture) |
@charlesroddie, the compiler-specific syntax is used to create specific compile paths for certain statically known types, and emit different IL for those. It's a bit more powerful than plain SRTP (and personally, I'd love it to be available to the general public, but that's another issue). The special-casing is, however, redundant. Each path creates a cast to The proposed simpler code removes this redundant compile time checking, and removes |
Quoting myself (#7958 (comment)):
|
@charlesroddie I don't think it's inefficient, the call should be eliminated by the JIT, and certainly in my proposed code. Your suggested code makes sense (if the syntax allows it), but it doesn't eliminate any boxing, and it still requires The proposed simplified code would become (#7958 (comment)): let anyToString nullStr x =
match box x with
| null -> nullStr
| :? System.IFormattable as f -> f.ToString(null,System.Globalization.CultureInfo.InvariantCulture)
| obj -> obj.ToString()
let string value =
anyToString "" value |
I've researched this a bit further for an up and coming PR. It turns out that the original code contained "dead code", anything after the This means that the current code is already equal to This made we wonder about the double null-check as originally reported here by @teo-tsirpanis. It turns out that writing this: match box x with
| null -> do something
| :? ISomething as foo -> foo.CallInterface() already leads to the double null-check:
Since this means that you don't need to check for let inline anyToString nullStr x =
match box x with
| :? System.IFormattable as f -> f.ToString(null,System.Globalization.CultureInfo.InvariantCulture)
| null -> nullStr
| obj -> obj.ToString()
let inline string value =
anyToString "" value This leads to the following decompiled C# code: public string newString1()
{
object obj = MixString.get(1); // this is from my test-setup
object obj2 = obj;
IFormattable formattable = obj2 as IFormattable;
if (formattable != null) // only one null-test now
{
IFormattable formattable2 = formattable;
return formattable2.ToString(null, CultureInfo.InvariantCulture);
}
if (obj2 != null) // and another one if it is not IFormattable
{
object obj3 = obj2;
return obj3.ToString();
}
return "";
} I was wondering if this would lead to any measurable performance gain or loss. I set the max-relative-error extra tight, but still there's no measurable difference, the timings are all inside the error range, and different runs give different results. First run: Second run: Since the inlining of Example of what's currently impossible: type Du<'T> =
| Foo of 'T
override this.ToString() = // FS0670 error shown here
match this with
| Foo x -> string x // caused by this |
@abelbraaksma I suppose you are talking about the Other than that, you proposal seems good. Still I haven't understood what is the difference between |
It's a static type param in the original, so the object is always known. And the code to do what you suggest was there, but was dead voice, never executed. However, it blocks several important use cases by raising FS0670 in generic contexts. With the current state of the compiler, it is not possible to have both.
I originally tried working on a solution that avoids this, but there's currently no way. And boxing is not a problem here, as calling the Also, If we add all these things together, and check the disassembly, we see there's no measurable difference. The timings support that too. Since usually functionality trumps performance, and, apart from a slighter larger disassembly and (sometimes) larger IL, there isn't a performance drop, I would go for the non inlined version, so that it can be used in all contexts. It's possible that at some point we'll introduce special casing for inlined generics that don't expose them (escape scope), (the same problem exists for piping, for instance with refs), and then we could reconsider this. |
I see. The behavior I describe apparently needs compiler support for intrinsics (i.e. statically determining at the compiler's side which code to emit) which I assume does not exist (at least on the It might be quite a big and complex feature but we must add it one day if we want to be serious about F#'s performance. Converting an object to a string is not a big deal itself, but the compiler could (and should) aggressively optimize code (an example that came to my mind is completely inlining the |
Yes and no. Yes, because using the I agree it would be nice to have both. Meaning, like in the runtime and the BCL, where it is possible to code an exact intrinsic, regardless whether the function is inlined or not. Btw, sometimes we have a little of both. For instance, There will always be a tension field between the best way to code, optimize and jit something. It takes a lot of research for each of these functions. But ultimately, step by step, things will improve (and imo, the optimizations are already quite darn good, from what I've seen in my recent research for the String.xxx optimization PR's).
This is usually true, for instance |
This simple code snippet:
produces this IL code that involves a redundant boxing and type checking:
The compiler knows that
4
is an integer and implementsIFormattable
. For value types and sealed classes known to implement it, emitting a direct call toToString
with the invariant culture passed (and theconstrained.
IL prefix) would be much better. Same with the eligible types that are known to not implement it.For all other cases (non-sealed reference types that do not implement
IFormattable
), the existing behavior is acceptable (no boxings) and backwards-compatible. Performance-concerned developers can always directly callToString()
.There might be a problem with types that might stop implementing
IFormattable
at a later version of the library, but this is a breaking change that should require recompilation and we should not be concerned with such scenarios.The text was updated successfully, but these errors were encountered: