-
Notifications
You must be signed in to change notification settings - Fork 21
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 Operators.string to be used in more places (prevent FS0670); optimize generated IL code #890
Comments
@KevinRansom, a specific note to you. After our talk on the current PR, you asked me to investigate if we could have better performance and less IL for the native types. I thought at the time that it was not possible to have (A) static resolution (needed for special-casing native types) and (B) allowing It turns out that getting both (A) and (B) are possible by using the compiler-only syntax of However, statically distinguishing between I'll update in the next hour or so in the PR, so you can see more detailed what I mean. |
@abelbraaksma , I don't know if we could un-inline it without suffering a performance degradation. [<CompiledName("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 : float = (# "" value : float #).ToString("g",CultureInfo.InvariantCulture)
when ^T : float32 = (# "" value : float32 #).ToString("g",CultureInfo.InvariantCulture)
when ^T : int64 = (# "" value : int64 #).ToString("g",CultureInfo.InvariantCulture)
when ^T : int32 = (# "" value : int32 #).ToString("g",CultureInfo.InvariantCulture)
when ^T : int16 = (# "" value : int16 #).ToString("g",CultureInfo.InvariantCulture)
when ^T : nativeint = (# "" value : nativeint #).ToString()
when ^T : sbyte = (# "" value : sbyte #).ToString("g",CultureInfo.InvariantCulture)
when ^T : uint64 = (# "" value : uint64 #).ToString("g",CultureInfo.InvariantCulture)
when ^T : uint32 = (# "" value : uint32 #).ToString("g",CultureInfo.InvariantCulture)
when ^T : int16 = (# "" value : int16 #).ToString("g",CultureInfo.InvariantCulture)
when ^T : unativeint = (# "" value : unativeint #).ToString()
when ^T : byte = (# "" value : byte #).ToString("g",CultureInfo.InvariantCulture) It's mainly because of statically resolved type parameters that the error occurs. But, we need to use statically resolved type parameters in order to use the internal/FSharp.Core-only generic specializations on the primitive types.; this is for performance optimizations. Otherwise, we would have to fallback to reflection. |
@TIHan, that was my original thought as well. However, have a look at the code you just posted, the first two lines. The first is hit for classes, the second four structs. None of the others are ever hit (confirmed by testing). anyToString "" value // general case and body of function
when ^T struct = anyToString "" value // special case for structs, lines after this are not hit
when ^T : float = (# "" value : float #).ToString("g",CultureInfo.InvariantCulture) // special case for float, never hit, because it is also a struct
when ^T: float32 ... // also not hit, etc So the status quo has a bug. But removing the struct line and placing it below creates a compat issue, because enums will not be printed correctly. I found a way to remove the 'inline' mark and still have the compiler issue the compiler optimizations. The linked PR has an implementation of that (I need to issue new timings though, they were from the original, simpler idea of only removing 'inline') |
For reference, It now won't raise an error when used in places where the "type could escape its scope", because of the change from [<CompiledName("ToString")>]
let inline string (value: 'T) =
anyToString "" value
when 'T : string = (# "" value : string #) // force no-op
// Using 'let x = (# ... #) in x.ToString()' leads to better IL, without it, an extra stloc and ldloca.s (get address-of)
// gets emitted, which are unnecessary. With it, the extra address-of-variable is not created
when 'T : float = let x = (# "" value : float #) in x.ToString(null, CultureInfo.InvariantCulture)
when 'T : float32 = let x = (# "" value : float32 #) in x.ToString(null, CultureInfo.InvariantCulture)
when 'T : decimal = let x = (# "" value : decimal #) in x.ToString(null, CultureInfo.InvariantCulture)
when 'T : BigInteger = let x = (# "" value : BigInteger #) in x.ToString(null, CultureInfo.InvariantCulture)
// no IFormattable
when 'T : char = let x = (# "" value : char #) in x.ToString()
when 'T : bool = let x = (# "" value : bool #) in x.ToString()
when 'T : nativeint = let x = (# "" value : nativeint #) in x.ToString()
when 'T : unativeint = let x = (# "" value : unativeint #) in x.ToString()
// For the int-types:
// It is not possible to distinguish statically between Enum and (any type of) int.
// This way we'll print their symbolic value, as opposed to their integral one
// E.g.: 'string ConsoleKey.Backspace' gives "Backspace", rather than "8")
when 'T : sbyte = (box value :?> IFormattable).ToString(null, CultureInfo.InvariantCulture)
when 'T : int16 = (box value :?> IFormattable).ToString(null, CultureInfo.InvariantCulture)
when 'T : int32 = (box value :?> IFormattable).ToString(null, CultureInfo.InvariantCulture)
when 'T : int64 = (box value :?> IFormattable).ToString(null, CultureInfo.InvariantCulture)
// unsigned integral types have equal behavior with ToString() vs IFormattable::ToString
// this allows us to issue the 'constrained' opcode with 'callvirt'
when 'T : byte = let x = (# "" value : 'T #) in x.ToString()
when 'T : uint16 = let x = (# "" value : 'T #) in x.ToString()
when 'T : uint32 = let x = (# "" value : 'T #) in x.ToString()
when 'T : uint64 = let x = (# "" value : 'T #) in x.ToString()
// other common mscorlib System struct types
when 'T : DateTime = let x = (# "" value : DateTime #) in x.ToString(null, CultureInfo.InvariantCulture)
when 'T : DateTimeOffset = let x = (# "" value : DateTimeOffset #) in x.ToString(null, CultureInfo.InvariantCulture)
when 'T : TimeSpan = let x = (# "" value : TimeSpan #) in x.ToString(null, CultureInfo.InvariantCulture)
when 'T : Guid = let x = (# "" value : Guid #) in x.ToString(null, CultureInfo.InvariantCulture)
when 'T struct =
match box value with
| :? IFormattable as f -> f.ToString(null, CultureInfo.InvariantCulture)
| _ -> value.ToString()
// other commmon mscorlib reference types
when 'T : StringBuilder = let x = (# "" value : StringBuilder #) in x.ToString()
when 'T : IFormattable = let x = (# "" value : IFormattable #) in x.ToString(null, CultureInfo.InvariantCulture) Before, when you did Note that the code above will optimize where it can:
|
Issue updated, previous comment updated, as new insights show that we can reach the same behavior without removing |
@TIHan, I think this is now ready. I verified the RFC to make sure your last comments have been reflected. Can this be "approved in principle"? It is a benign change (if it is a change at all). |
Yes, for some reason I had thought that was already done |
RFC was merged and can now be found here: https://github.com/fsharp/fslang-design/blob/master/preview/FS-1089-allow-string-everywhere.md. I'll close this, as it's now done and implemented. |
Allow use of 'string' everywhere, and optimize generated IL
I propose:
that we change
^T
to'T
from theOperators.string
function. This allows its usage in places where you would now get an error that it "escapes its scope" (report: Usingstring obj
vs obj.ToString() leads to problems: the type parameter can escape its scope dotnet/fsharp#7958). Consider:this code fails to compile with:
that we optimize the generated IL. Currently, due to what can be considered a bug, regardless of input, the generated IL is huge (report: The 'string' operator produces suboptimal code when the object's type is known. dotnet/fsharp#9153 Since this is a statically-inlined function, each time you call
string
, it expands to this:But need to be only this in most cases:
that we improve speed. Currently, a double null-check is done. This is unnecessary, in fact, many code paths don't need a null-check at all. It would also be nice to remove
callvirt
and usecall
instead (or aconstrained callvirt
), in cases where this is possible.I was already implementing this (see dotnet/fsharp#9549), but while doing so, and discussing it with @KevinRansom, we found that there's a tension between the above requests and that not all of them can be supported for all scenarios. This is in part due to the nature of
enum
: it's treated as one of eight integer types.The existing way of approaching this problem in F# is: roll your own
string
function (this has long been my approach in my own code, and this is easy if you don't have to deal with case-printing forenum
).Details are in a tentative RFC.
Pros and Cons
The advantages of making this adjustment to F# are:
string
everywhere (except with refs, but that's another discussion)int
,float
etc that are never hitThe disadvantages of making this adjustment to F# are:
inline
string x
, wherex
is an input parameter, if never used will be inferred as typeobj
. After this change, it will remain generic (this is more of advantage than a disadvantage, I think)Extra information
Estimated cost (XS, S, M, L, XL, XXL): XS
While researching this was more challenging than I thought, the final changes will be just a few lines, and probably some new test cases.
Related suggestions: None (but see the linked issues for the bug reports that lead to this)
Of note:
string obj
vs obj.ToString() leads to problems: the type parameter can escape its scope dotnet/fsharp#7958 (comment)Affidavit (please submit!)
Please tick this by placing a cross in the box:
Please tick all that apply:
The text was updated successfully, but these errors were encountered: