-
Notifications
You must be signed in to change notification settings - Fork 4.9k
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: devirtualization next steps #7541
Comments
Some data from crossgen of System.Private.CoreLib, after dotnet/coreclr#10192. This shows the relative likelihood of devirtualization success or failure based on the operation feeding the call. The accounting is a bit tricky because this data includes callsites in inline candidates -- so from an IL standpoint some sites may be counted multiple times. Overall around 8% of call sites are getting devirtualized. Virtual calls fare a bit better than interface calls. A number of operators never supply types. It's not easy to know what a realistic upper bound is on how many sites can be devirtualized, but my feeling is that there is still quite a bit of headroom left and a number like 12% might not be out of the question.
Devirtualization can fail for several reasons; here's a crude accounting:
We should be able to reduce all of these numbers with improved tracking of types in the jit. |
Update after dotnet/coreclr#10371. There is a marked decrease in interface success because we cannot safely devirtualize interface calls just with a final (sealed) method -- we need a final or exact class. Explicit interface implementations in C# mark the corresponding methods as final so this was incorrectly firing a lot before the fix.
(edit: had virtual/interface labels swapped; now fixed) |
More detailed breakdown, showing reasons and operations (reasons lower case, operators upper case , each partitions the total above it).
|
A blend of Devirtualize interface calls/Devirtualize calls on struct types. Currently for a generic you can do: T value
((int)(object)value) == ((int)(object)value) And it will undo the object box for structs; however that requires a specific concrete type Would an aim be to also work an generic interface/constraint so if T value
((IEquatable<T>)(object)value).Equals(value) |
Yes. We have some of the parts need for this, but not all. Today the jit produces
and devirt is blocked because the jit does not yet understand what type the interface cast returns. So the first bit is to teach the jit what this helper does- - I've been holding off because generally figuring out when a cast will always succeed or will always fail is not that easy and there are lots of these cast and isinst helpers. But maybe I should push ahead get the easier cases out of the way. This will require new jit -> vm queries for casting which will return yes/no/maybe answers; initially there will be a lot of maybes but we can improve over time. If we do that and get back a yes then the cast is a no-op and we know the type going into the call. So we can devirtualize the call and remove the cast. However, we will still be boxing and calling the boxed entry point for the devirtualized We should be able to inline, and one might hope that at this point if we do inline, we could propagate the value being boxed through the box to the ultimate use, then realize the box is dead, and remove it. Some of this might actually happen, at least for primitive types in simple cases. I'd have to try and see. We should also be able to see that the boxing done here is "harmless" because Equals won't modify its I have work in progress branches for some of the missing parts, so am slowly chipping away at this. |
Hmm, we don't undo the box here after inlining, but we're not that far off from being able to: using System;
class D
{
static bool TestMe(object o)
{
return ((int) o) == 33;
}
public static int Main()
{
return TestMe(33) ? 100 : 0;
}
} Need importation of Unbox.Any to try and optimize away its type test. |
Even if the cast will "maybe" succeed, the return type is still known if the cast throws (e.g., In cases where it's unfeasible to prove that the unboxed value isn't modified, a value copy would work and still avoid boxing. If the cast result is always used as a unboxed value type, then even the helper call could work on a fake box (on stack). |
We usually don't learn much about an object's type from successful a interface cast other than the fact that that the type indeed implements the interface. And yeah, making a "fake" box would be a reasonable way to handle possible side effecting calls. |
In a generic though, you also know what the actual type is? (if struct) |
Yes, but (if I'm following you correctly) we knew that before the cast -- the question is whether the cast tells us anything new if it succeeds. A successful "maybe" cast to a non-interface type tells us something new, and if we're lucky, might allow us to devirtualize interface calls on code guarded by the cast. A successful "maybe" cast to an interface type doesn't. |
Expressing it in il, I'm suggesting that bool DoThing2<T>(T val0, T val1)
{
return ((IEquatable<T>)val0).Equals(val1);
}
When bool DoThing1<T>(T val0, T val1) where T : IEquatable<T>
{
return val0.Equals(val1);
} e.g.
by dropping
and inserting
before the call I'm not suggesting its that straight forward 😄 Is more to capture what I mean |
Though in actual use it would have to be guarded bool DoThing2<T>(T val0, T val1)
{
if (typeof(IEquatable<T>).IsAssignableFrom(typeof(T)))
{
return ((IEquatable<T>)val0).Equals(val1);
} il
And changing all that, is probably a much bigger thing - so it might not be practically worth it :-/ |
The first part is more or less where things are headed. If we know for sure T implements the interface and T is a value class then we might be able to determine which method will be called. So at least in some cases we should be able to devirtualize and unbox and inline and all that. |
Is it correct that generics don't currently devirtualize? I can't seem to get ArrayPool.Shared to devirtualize; even if I pull the Byte and Char pool out to a non-generic class, and reference them directly internal sealed class ArrayPools
{
internal static TlsOverPerCoreLockedStacksArrayPool<byte> BytePool { get; } = new TlsOverPerCoreLockedStacksArrayPool<byte>();
internal static TlsOverPerCoreLockedStacksArrayPool<char> CharPool { get; } = new TlsOverPerCoreLockedStacksArrayPool<char>();
}
I don't see
Which I was expecting (and definitely can't get it to work through Not a covered scenario yet? Or are there further (or less) changes I could make to the source to trigger it? |
Looking at the crossgened (rather than jitted) output so that might not be helping |
The explanation will be there in the jit dump. I recall seeing some array pool methods devirtualizing in the past. I should probably surface the reasons why methods don't devirtualize... most don't so the level of chatter will go up quite a bit. |
This definition: public static ArrayPool<T> Shared { get; } =
typeof(T) == typeof(byte) || typeof(T) == typeof(char) ? new TlsOverPerCoreLockedStacksArrayPool<T>() :
Create(); creates a hidden backing static field initialized by the class constructor. So when
If you call byte[] b = ArrayPool<byte>.Create().Rent(33);
;; Devirtualized virtual call to System.Buffers.ArrayPool`1[Byte]:Rent; now direct call to System.Buffers.ConfigurableArrayPool`1[Byte][System.Byte]:Rent [exact] |
Which is why I tried to push it out to strongly typed fields in dotnet/coreclr@dea46f4 (the most extreme variant I was trying) public static ArrayPool<T> Shared
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get
{
if (typeof(T) == typeof(byte))
{
return (ArrayPool<T>)(object)ArrayPools.BytePool;
}
else if (typeof(T) == typeof(char))
{
return (ArrayPool<T>)(object)ArrayPools.CharPool;
}
else
{
return s_generalPool;
}
}
} Also tried two fields which avoided the cast through public static ArrayPool<T> Shared
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get
{
if (typeof(T) == typeof(byte) || (typeof(T) == typeof(char))
{
return s_specificPool;
}
else
{
return s_generalPool;
}
}
} |
Will explore with Jitdump; I think devirtualization will give much more benefit than for example using Lzcnt dotnet/coreclr#15685 |
Looks like (with your dea46f4) we lose track of the type for the return value from
Later when we've inlined
Likely we need to update the code in |
Made PR to reduce my noise in this issue dotnet/coreclr#15743 |
If the jit sees that an inlinee has multiple return sites or has gc ref locals it will choose to return the inline result via a temp. The jit was not assigning a type to that temp and so losing track of some type information. So, for inlinees returning ref types, initially type the return spill temp with the declared return type of the method. When importing we may discover that particular return sites will return more specific types. If all discovered return sites agree, we can update the return type for the spill temp to match the consensus improved type. This can lead to removal of some type checks and also to devirtualization. Addresses issues discussed in #9908 and dotnet#15743.
If the jit sees that an inlinee has multiple return sites or has gc ref locals it will choose to return the inline result via a temp. The jit was not assigning a type to that temp and so losing track of some type information. So, for inlinees returning ref types, initially type the return spill temp with the declared return type of the method. When importing we may discover that particular return sites will return more specific types. If all discovered return sites agree, we can update the return type for the spill temp to match the consensus improved type. This can lead to removal of some type checks and also to devirtualization. Addresses issues discussed in #9908 and dotnet#15743.
If the jit sees that an inlinee has multiple return sites or has gc ref locals it will choose to return the inline result via a temp. The jit was not assigning a type to that temp and so losing track of some type information. So, for inlinees returning ref types, initially type the return spill temp with the declared return type of the method. When importing we may discover that particular return sites will return more specific types. If all discovered return sites agree, we can update the return type for the spill temp to match the consensus improved type. This can lead to removal of some type checks and also to devirtualization. Addresses issues discussed in #9908 and #15743.
A couple of other bits of data: devirtualization works much better for call sites in inlinees than in the root method. Not too surprising. Note devirtualization and inlining are interleaved...so a devirtualization often leads to an inline where we know the exact type of Also late devirtualization doesn't happen very often, and isn't very successful. Probably worth a deeper look. Recall late devirtualization is done after an inline, where the jit propagates (better) information about the callee return type into the calling method. The low success rate here could have multiple causes -- for instance the jit doesn't do a thorough job of converting all shared types to unshared types, so if we inline a shared generic that returns |
@dotnet/jit-contrib |
@AndyAyersMS are there any ETW events for devirtualization similar to the ones we have for inlining? I am asking because I recently wrote an ETWProfiler for BenchmarkDotNet which allows to profile the benchmarks and calculate some metrics based on the captured ETW event. If there is any interesting info in the trace file related to devirtualization I could calculate new metrics and include them in BenchView for our benchmarks (/cc @jorive) |
No, there aren't any ETW events. I probably should add something to the inline tree but haven't done that yet either. |
PR to added some info to the inline tree: dotnet/coreclr#20395 |
Now merged, so the jit is tracking this information internally at least:
|
Some further improvements over in dotnet/coreclr#20447. |
FIrst steps towards guarded devirtualization: dotnet/coreclr#21270 |
@AndyAyersMS I really appreciate the hard work you're doing man! (you and everyone here actually) |
@rikimaru0345 thanks for the kind words -- good to hear things are working well for you. |
How does the CLR treat Default Interface Members when you use a reference to such interface AND the interface was implemented through a ValueType? Will it be devirtualized too? interface I
{
public int X() { return 0; }
}
struct V : I { }
...
I iface = new V(); // Note: it is a **ValueType**
return iface.X(); // Devirtualization here? If so, how? Would that run the same (or nearly) as a struct-defined method? Anyway, thanks for your hard work on this! |
(assuming Lazy is not already intrinsified) Would this cover cases like
|
Linking #110827 here where it enabled inlining for late devirted calls |
dotnet/coreclr#9230 introduced some simple devirtualization. Some areas for future enhancements:
this
pointer in the call to escape. So we can perhaps stack-allocate the object in its boxed form and pass that. Here we'd be counting on the fact that methods on mutable GC value types should already be using checked write barriers for updates.fgFindBasicBlocks
the jit could count how many times each local appears in astarg
orldarga
instruction. If there is just onestarg
(which should be a fairly common case) then at thestarg
the jit can check the type of the value being assigned and use it if more precise than the local's declared type. This in this case it is also safe to propagate the exactness flag. (Some of this now done, see JIT: improve types for single def locals and temps coreclr#10471, Jit: fix a few issues with single def local tracking coreclr#10633, Jit: fix issue with single-def type propagation coreclr#10867, Track single def locals in importer coreclr#21251)List<T>.get_Item
returnsT
. In many cases the caller will know the type ofT
exactly, but currently the jit ends up with the shared generic ref type placeholder type__Canon
. There's an example of this in thefromcollections
regression test. See JIT: preliminaries to improve types coreclr#10172 for a partial fix. Fixed by Pass exact context to getMethodInfo #88025.ldvirtftn
and calls that internally translate toldvirtftn
(Generic virtual method devirtualization does not happen #32129)category:cq
theme:devirtualization
skill-level:expert
cost:extra-large
impact:large
The text was updated successfully, but these errors were encountered: