-
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
JIT: "is" keyword generates boxing in generic function #8576
Comments
This is because I guess the JIT can be improved to remove unused codepaths predicated on |
It definitely sounds like a worthy optimization to me... One additional comment - with the new pattern matching the impact of this additional allocation may be even worse, as people use it more. |
Handling I've been playing with some prototypes here, but nothing worth calling attention to quite yet. |
@AndyAyersMS Wouldn't restricting the optimization to struct/primitive types be an acceptable tradeoff here? |
Yes, looking at value classes here makes sense, since the box is the expensive bit. My prototyping was at the |
With dotnet/coreclr#13169 my prototype gets the example above and also can now clean up after the box. However, aside from the example above, I'm not seeing any cases so far where a value type box feeds an isinst or castclass. It's possible I am missing out on cases that could be optimized because the ability of the jit to reason about types is still somewhat limited (see for instance #7816). For example when crossgenning System.Private.CoreLib the jit sees around 3100 optimizable istinst/castclass operations, and is able to determine the (approximate) type of the object being tested in around 1400 cases. From that the jit is able to successfully determine the result of the cast in only 2 cases. I will revisit as I improve local type propagation but unless the cases with missing types are somehow correlated with the optimizable cases it seems unlikely the overall success rate will improve. @roji was this just a "hey, that's unexpected" kind of observation? Or can you refer me to examples where this pattern happens frequently? |
Prototype is on this fork, for future reference. Output on the case above is ### M op BOX kind isinst have System.Int32 want System.String Optimizing FAILURE, unrelated types
; Assembly listing for method Program:Foo(int)
; Emitting BLENDED_CODE for X64 CPU with AVX
; optimized code
; rsp based frame
; partially interruptible
; Final local variable assignments
;
;* V00 arg0 [V00 ] ( 0, 0 ) int -> zero-ref
;* V01 tmp0 [V01 ] ( 0, 0 ) ref -> zero-ref
;# V02 OutArgs [V02 ] ( 1, 1 ) lclBlk ( 0) [rsp+0x00]
;
; Lcl frame size = 0
G_M45348_IG01:
G_M45348_IG02:
C3 ret
; Total bytes of code 1, prolog size 0 for method Program:Foo(int) |
I suspect the relative rarity of this pattern is caused in part by people actively avoiding it because they know it allocates. |
@pentp first, I'm not sure we know much about the relative rarity of this pattern, and in any case I would highly doubt people are aware of this boxing allocation - it is extremely unlikely to me that someone would assume that an @AndyAyersMS I ran into this issue while doing memory profiling on my project, where a generic function needed some specialized logic. Of course I have no idea how frequent specialization is, but as I said above I can imagine it becoming a problem with the new pattern matching feature. For example, I would expect that in contexts needing specialization (like mine) you'd fine the following pattern: if (t is string s) {
// string specialization
} The alternative non-allocating alternative would be to compare types instead, but that would imply an additional cast which pattern matching is intended to avoid. |
I have avoided this pattern for many years and I didn't assume - I checked the x86 asm after I saw a |
The problem here is that unlike most other boxing scenarios where the need for boxing is intuitive to the programmer, this one is completely unexpected and needless (which is why this issue exists). I would expect the number of programmers which regularly review IL/asm to be quite low. |
Would be nice if this worked without boxing (allocates 24 bytes) int key0, key1;
PatternMatchEquality<int>.Equals(key0, key1) Where public class PatternMatchEquality<TKey>
{
private static readonly EqualityComparer<TKey> _defaultComparer = EqualityComparer<TKey>.Default;
public static bool Equals(TKey key0, TKey key1)
{
switch (key0)
{
case IEquatable<TKey> key:
return key.Equals(key1);
default:
//return _defaultComparer.Equals(key0, key1);
throw new Exception();
}
}
} Though is the issue the C# compiler doesn't emit a
|
Raised issue for Though would still have the issue with boxing for |
I just updated my branch for this yesterday; let me see how it fares on this case. Main concern with the proposed change is whether I have the logic for |
Building :) |
The fork's version is not updated, sorry.... anyways the local update doesn't get the above case because it is looking for a specific cast helper and that rules out interface checks. |
:( |
I can generalize the checks, but will need some new methods on the jit interface so the jit can call back to the EE to ask questions about whether or not casts won't/may/must succeed. |
As background... The scenario I'm trying to cover is calling struct generics if the type implements the the interface; so in effect getting the benefit of having a generic constraint without one being present. The example I was looking at was the default comparer on dictionary; which currently goes via hoops and inheritance to implement the constrained However while that works, it also ends up with virtual calls for
As highlighted by ravendb in their fast-dictionary blog post; and the same thing could be achieved with the default comparer and struct keys in the regular dictionary. Using a non-inherited internal class DictionaryEquatableComparer<T> where T : IEquatable<T>
{
public bool Equals(T x, T y) => x.Equals(y);
public int GetHashCode(T obj) => obj.GetHashCode();
}
public class OverridableComparer<TKey> where TKey : IEquatable<TKey>
{
private static readonly DictionaryEquatableComparer<TKey> s_comparer = new DictionaryEquatableComparer<TKey>();
private IEqualityComparer<TKey> _comparer;
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool KeyEquals(TKey key0, TKey key1)
{
return _comparer == null ? s_comparer.Equals(key0, key1) : _comparer.Equals(key0, key1);
}
} However can't be used in dictionary as So was looking to see if pattern matching could solve it; as at Jit time everything is known about the key type (if struct; i.e. whether it implements That was the hope and motivation anyway :) |
Specifically I'd like to do something like this for private static readonly EqualityComparer<TKey> s_defaultComparer;
private readonly IEqualityComparer<TKey> _customComparer;
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool KeyEquals(TKey key0, TKey key1)
{
bool result;
if (_customComparer == null)
{
switch (key0)
{
case IEquatable<TKey> key:
result = key.Equals(key1);
break;
default:
result = s_defaultComparer.Equals(key0, key1);
break;
}
}
else
{
result = _customComparer.Equals(key0, key1);
}
return result;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public int KeyHashCode(TKey key)
{
int result;
if (_customComparer == null)
{
switch (key)
{
case IEquatable<TKey> k:
result = k.GetHashCode();
break;
default:
result = s_defaultComparer.GetHashCode(key);
break;
}
}
else
{
result = _customComparer.GetHashCode(key);
}
return result & 0x7FFFFFFF;
} |
Interesting -- I had been planning on looking at ways t the jit could improve dictionary perf for simple key types and this might be the way to go. |
Don't think its currently expressible in C# as above would be var key = key0 as IEquatable<TKey>
return key.Equals(key1); 😢 Working on proposal |
Proposal dotnet/csharplang#905 |
For Dictionary's case we could have a tactical solution . Jan Kotas mentioned that it was already implemented by .NET Native for UWP. |
@omariom Thanks for the reminder. That is one of the things I want to look into further. |
dotnet/coreclr#14178 hits this as well. |
Implement the jit interface compareTypesForEquality method to handle casts from known types to known types, and from shared types to certain interface types. Call this method in the jit for castclass and isinst, using `gtGetClassHandle` to obtain the from type. Optimize sucessful casts and unsuccessful isinsts when the from type is known exactly. Undo part of the type-equality based optimization/workaround in the AsyncMethodBuilder code that was introduced in dotnet#14178 in favor of interface checks. There is more here that can be done here before this issue is entirely closed and I will look at this subsequently. This optimization allows the jit to remove boxes that are used solely to feed type casts, and so closes #12877.
It seems that using the
is
keyword in a generic function causes value types to be boxed. Consider the following program:This is especially odd as comparison to null doesn't box. This can be worked around by avoiding
is
and comparing types (and can also provide a form of "template specialization" as in https://github.com/MikePopoloski/StringFormatter/blob/c36fb18edd44b4d2c75e210e85dc93a3fd58efe0/StringFormatter/StringBuffer.cs#L523).The text was updated successfully, but these errors were encountered: