-
Notifications
You must be signed in to change notification settings - Fork 5.2k
Description
The jit cannot devirtualize interface calls made through arrays, because this aspect of arrays has a special implementation in the runtime. I noticed this a while back but never wrote up anything. I was reminded of this again by #62266.
Consider a simple program like:
using System;
using System.Collections.Generic;
class X {
public static void Main() {
IList<int> list = new int[10];
for (int i = 0; i < 100; i++) {
foreach (var _ in list) {
// nothing
}
}
}
}Here all types are known and we should be able to de-abstract this and come close to the equivalent for loop.
Currently none of the interface calls devirtualize or inline, the method allocates, and the try/finally can't be optimized away, despite the empty Dispose from the enumerator.
Fixing all this requires resolving a number of issues:
resolveVirtualMethodHelperon the jit interface callsCanCastToInterface-- this bypasses special checks inCanCastTothat handle interface methods for arrays.- Once that is fixed, the method that gets devirtualized to is
SZArrayHelper.GetEnumerator<T>which is a generic method, not an instance method on a generic class. So the context handling inresolveVirtualMethodHelperneeds to be updated here and also over inimpDevirtualizeMethod-- currently we always assume class context. - Similar changes are needed in crossgen2 (haven't looked into this yet)
- Once that is fixed, the jit can devirtualize the
GetEnumeratorcall, but this happens "too late" to devirtualize the subsequentMoveNextandget_Currentcalls through the enumerator. The fix here is likely to mark theseGetEnumeratormethods as intrinsics and extendgetSpecialIntrinsicExactReturnTypeto propagate the right type downstream during importation. - The
GetEnumeratorcall is too big to inline by default. Likely we can just mark this withAggressiveInlining. But pragmatically we might want to boost inlining for methods that return a type (especially a sealed type) that is more derived that the declared return type. Doing so likely requires resolving more tokens during the pre-inline scan, but perhaps we're already doing this fornewobjand it might not be too costly. - If we inline (and haven't fixed (4) yet) then we can late devirtualize the
MoveNextandDisposeas we see the improved type flowing out of the inlined method -- but not the call toget_Current-- not sure why just yet. - The enumerator is a class, not a struct; if we made it a struct we might have a shot at removing the boxing (rather than proving we can stack allocate a class). But this box would have multiple uses, so we'd also need to tackle JIT: optimizations for multi-use boxes #9118.
- It remains to be seen if we actually do inline all those methods and undo the boxing whether we can then enable struct promotion and remove the overhead of iterating via an enumerator.
I will point out that the "equivalent" direct code (with loop non-empty to avoid it being optimized away entirely)
public static int M_enum()
{
int[] a = new int[1000];
int result = 0;
foreach (var v in a)
{
result ^= v;
}
return result;
} is marginally less efficient than the for loop version:
public static int M()
{
int[] a = new int[1000];
int result = 0;
for (int i = 0; i < a.Length; i++)
{
result ^= a[i];
}
return result;
}In the more general case where we don't know the exact collection class, we might hope PGO would get us to an equivalent place. But here there are additional challenges.
- The enumerator GDV doesn't allow us to know the enumerator type in the subsequent loop. We ought to notice a correlation between the type of the enumerator returned and the class type seen in the loop interface calls (or the fact that the latter are invariants) and decide to clone the loop based on a type test. That would remove the GDV tests from within the loop.
- Ideally then we see that the loop cloning check is partially redundant based on the outcome of the
GetEnumeratorGDV test and simplify that path further. So, in the "good" case we do one type test to see what kind of collection we have, and then branch to a specialized loop that knows exactly what kind of enumerator we have.
This is perhaps the simplest case of de-abstraction. Other collection types have more complex enumerators.
category:cq
theme:devirtualization
skill-level:expert
cost:small
impact:medium
Metadata
Metadata
Assignees
Labels
Type
Projects
Status