-
Notifications
You must be signed in to change notification settings - Fork 5k
JIT: Support for devirtualizing array interface methods #108153
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
Conversation
Update JIT and runtime to devirtualize interface calls on arrays over non-shared element types. Shared types are not (yet) handled. Add intrinsic and inlining attributes to key methods in the BCL. This allows the JIT to devirtualize and inline enumerator creation and devirtualize and inline all methods that access the enumerator. And this in turn allows the enumerator to be stack allocated. However, the enumerator fields are not (yet) physically promoted, because of an optimization in the BCL to return a static empty array enumerator. So the object being accessed later is ambiguous. Progress towards dotnet#62457.
Tagging subscribers to this area: @JulieLeeMSFT, @jakobbotsch |
using BenchmarkDotNet.Attributes;
public class Bench
{
IList<int> list = new int[10];
[Benchmark]
public void Test()
{
foreach (var _ in list) {}
}
} |
I wouldn't expect much perf from this PR, unless perhaps the array is in a local (say an arg)... but would expect the enumerator to be on the stack so a bit less allocation. Ah, forgot that we are also devirtualizing / inlining, so I guess it does help a bit (been focusing on the stack alloc / promotion end of things lately). |
well, it does show an improvement EgorBot/runtime-utils#94 (comment) (forgot to add memory analyzer) |
It's not handling the GDV case right (at least sometimes)... need to dig in a bit more. |
The complication here is that GDV resolves the virtual call twice, and expects to get similar results both times, but for array interface calls the first devirt switches things over to the nonvirtual SZArrayHelper method. The second call needs to take pains to exactly match that.
The JIT ends up calling
For array interface methods the second call produces outputs that look nothing like the inputs, eg an interface + virtual method is mapped to a concrete class SZArrayHelper and a generic method. So when we get to step (3) we can't just use step (2)'s outputs as the inputs; we need to use the same inputs as step (2). That's more or less what my last commit does. A separate issue we should look at is why we can't just remember everything from (2) and not reinvoke in (3)... One issue is that (3)'s invocation is done in the middle of |
This seems to mostly work, but unfortunately doesn't enable stack allocation. The problem now is that the |
Fix for that is to remember the original context in the GDV info, and use that (plus original method) if we're trying to (re)-devirtualize an array interface call. With that, we can now (once again) inline For GDV we also see the non-GDV paths lower down, and because of that, we think the array enumerator might escape (escape analysis is flow-insensitive...). |
A bit more on the above. The path A->B->B1->D->F is infeasible, but escape analysis doesn't know that, so it thinks that the Note the path A->C->D->E is feasible (as is A->B->B2->D->E), so even in the inlined code in E it is unclear which object is being accessed: it could be the newobj, the static, or some other other SZGenericArrayEnumerator returned by the interface call. Without GDV the path A->C->D is gone, which means the type of |
Something is broken in the host testing, it is trying to add symbolic links and these are failing, eg
|
/azp run runtime |
Azure Pipelines successfully started running 1 pipeline(s). |
That looks like a separate problem. I have a fix in #108379. With that and your CorInfoImpl.cs change, this devirts fine!
Native AOT fortunately doesn't do the "non-generic interface call leads to a generic method" thing. |
Nice! Thanks for looking deeper. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM! I need the changes to recognize [Intrinsic] after devirtualization too 🙂. Should we run PGO outerloops for it? (and, presumably, NativeAOT outerloop now)
This comment was marked as resolved.
This comment was marked as resolved.
@EgorBot -intel using BenchmarkDotNet.Attributes;
[MemoryDiagnoser]
public class Bench
{
IList<int> list = new int[1024];
[Benchmark]
public int Test()
{
int sum = 0;
foreach (var i in list)
sum += i;
return sum;
}
} |
Is there something that prevents this kicking in for crossgen2? Do we need to set the "is array devirt" in |
I haven't debugged it yet, but here's what the jit sees for R2R:
|
I see, so it gets rejected during devirtualization. Psychic debugging tells me it's because of this: runtime/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/ReadyToRunCompilerContext.cs Lines 162 to 165 in e690777
Should be fine then. |
@MichalStrehovsky are you going to sign off? You got added as a reviewer. |
Only because of the CorInfoImpl.cs change. That diff LGTM. |
…08379) We avoid devirtualizing into abstract classes because whole program view might have optimized away the method bodies and devirtualizing them doesn't lead to anything good. However, if the whole program view had a non-abstract child of this, we can no longer optimize this out and devirtualization should be fine. Fixes issue encountered in #108153 (comment)
Update JIT and runtime to devirtualize interface calls on arrays over non-shared element types. Shared types are not (yet) handled. Add intrinsic and inlining attributes to key methods in the BCL. This allows the JIT to devirtualize and inline enumerator creation and devirtualize and inline all methods that access the enumerator. And this in turn allows the enumerator to be stack allocated in some simple cases. However, the enumerator fields are not (yet) physically promoted, because of an optimization in the BCL to return a static empty array enumerator. So the object being accessed later is ambiguous. Alse ensure that since GDV resolves the virtual call twice, and expects to get similar results both times, things work for the array case by keeping track of the initial devirtualization inputs. Progress towards dotnet#62457.
…tnet#108379) We avoid devirtualizing into abstract classes because whole program view might have optimized away the method bodies and devirtualizing them doesn't lead to anything good. However, if the whole program view had a non-abstract child of this, we can no longer optimize this out and devirtualization should be fine. Fixes issue encountered in dotnet#108153 (comment)
Update JIT and runtime to devirtualize interface calls on arrays over non-shared element types. Shared types are not (yet) handled. Add intrinsic and inlining attributes to key methods in the BCL. This allows the JIT to devirtualize and inline enumerator creation and devirtualize and inline all methods that access the enumerator. And this in turn allows the enumerator to be stack allocated in some simple cases. However, the enumerator fields are not (yet) physically promoted, because of an optimization in the BCL to return a static empty array enumerator. So the object being accessed later is ambiguous. Alse ensure that since GDV resolves the virtual call twice, and expects to get similar results both times, things work for the array case by keeping track of the initial devirtualization inputs. Progress towards dotnet#62457.
…tnet#108379) We avoid devirtualizing into abstract classes because whole program view might have optimized away the method bodies and devirtualizing them doesn't lead to anything good. However, if the whole program view had a non-abstract child of this, we can no longer optimize this out and devirtualization should be fine. Fixes issue encountered in dotnet#108153 (comment)
Found regressions: |
Update JIT and runtime to devirtualize interface calls on arrays over non-shared element types.
Shared types are not (yet) handled.
Add intrinsic and inlining attributes to key methods in the BCL. This allows the JIT to devirtualize and inline enumerator creation and devirtualize and inline all methods that access the enumerator.
And this in turn allows the enumerator to be stack allocated.
However, the enumerator fields are not (yet) physically promoted, because of an optimization in the BCL to return a static empty array enumerator. So the object being accessed later is ambiguous.
Progress towards #62457.