-
Notifications
You must be signed in to change notification settings - Fork 4.9k
Add .NET 10 Preview 2 release notes -- Libraries and Runtime #9770
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
Changes from 2 commits
606bf0a
4a33b18
195a9b1
4efffd6
22c65e9
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | |||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -8,6 +8,160 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
|||||||||||||||||||||||||||||||||||||||||||||||||||
| - [What's new in .NET 10](https://learn.microsoft.com/dotnet/core/whats-new/dotnet-10/overview) documentation | |||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
|||||||||||||||||||||||||||||||||||||||||||||||||||
| ## Feature | |||||||||||||||||||||||||||||||||||||||||||||||||||
| ## Array Enumeration De-Abstraction | |||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
|||||||||||||||||||||||||||||||||||||||||||||||||||
| This is about the feature | |||||||||||||||||||||||||||||||||||||||||||||||||||
| Preview 1 introduced enhancements to the JIT compiler's devirtualization abilities for array interface methods, enabling the JIT to begin removing the abstraction overhead of array iteration via enumerators. Consider the following benchmarks: | |||||||||||||||||||||||||||||||||||||||||||||||||||
| ```csharp | |||||||||||||||||||||||||||||||||||||||||||||||||||
| public class ArrayDeAbstraction | |||||||||||||||||||||||||||||||||||||||||||||||||||
| { | |||||||||||||||||||||||||||||||||||||||||||||||||||
| static readonly int[] array = new int[512]; | |||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
|||||||||||||||||||||||||||||||||||||||||||||||||||
| [Benchmark(Baseline = true)] | |||||||||||||||||||||||||||||||||||||||||||||||||||
| public int foreach_static_readonly_array() | |||||||||||||||||||||||||||||||||||||||||||||||||||
| { | |||||||||||||||||||||||||||||||||||||||||||||||||||
| int sum = 0; | |||||||||||||||||||||||||||||||||||||||||||||||||||
| foreach (int i in array) sum += i; | |||||||||||||||||||||||||||||||||||||||||||||||||||
| return sum; | |||||||||||||||||||||||||||||||||||||||||||||||||||
| } | |||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
|||||||||||||||||||||||||||||||||||||||||||||||||||
| [Benchmark] | |||||||||||||||||||||||||||||||||||||||||||||||||||
| public int foreach_static_readonly_array_via_interface() | |||||||||||||||||||||||||||||||||||||||||||||||||||
| { | |||||||||||||||||||||||||||||||||||||||||||||||||||
| IEnumerable<int> o = array; | |||||||||||||||||||||||||||||||||||||||||||||||||||
| int sum = 0; | |||||||||||||||||||||||||||||||||||||||||||||||||||
| foreach (int i in o) sum += i; | |||||||||||||||||||||||||||||||||||||||||||||||||||
| return sum; | |||||||||||||||||||||||||||||||||||||||||||||||||||
| } | |||||||||||||||||||||||||||||||||||||||||||||||||||
| } | |||||||||||||||||||||||||||||||||||||||||||||||||||
| ``` | |||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
|||||||||||||||||||||||||||||||||||||||||||||||||||
| In `foreach_static_readonly_array`, the type of `array` is transparent, so it is easy for the JIT to generate efficient code. In `foreach_static_readonly_array_via_interface`, the type of `array` is hidden behind an `IEnumerable`, introducing an object allocation and virtual calls for advancing and dereferencing the iterator. In .NET 9, this overhead impacts performance profoundly: | |||||||||||||||||||||||||||||||||||||||||||||||||||
| ``` | |||||||||||||||||||||||||||||||||||||||||||||||||||
| | Method | Mean | Ratio | Allocated | | |||||||||||||||||||||||||||||||||||||||||||||||||||
| |------------------------------------------------------------- |-----------:|------:|----------:| | |||||||||||||||||||||||||||||||||||||||||||||||||||
| | foreach_static_readonly_array (.NET 9) | 153.4 ns | 1.00 | - | | |||||||||||||||||||||||||||||||||||||||||||||||||||
| | foreach_static_readonly_array_via_interface (.NET 9) | 781.2 ns | 5.09 | 32 B | | |||||||||||||||||||||||||||||||||||||||||||||||||||
| ``` | |||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
|||||||||||||||||||||||||||||||||||||||||||||||||||
| Thanks to improvements to the JIT's inlining, stack allocation, and loop cloning abilities (all of which are detailed in [dotnet/runtime #108913](https://github.com/dotnet/runtime/issues/108913)), the object allocation is gone, and runtime impact has been reduced substantially: | |||||||||||||||||||||||||||||||||||||||||||||||||||
| ``` | |||||||||||||||||||||||||||||||||||||||||||||||||||
| | Method | Mean | Ratio | Allocated | | |||||||||||||||||||||||||||||||||||||||||||||||||||
| |------------------------------------------------------------- |-----------:|------:|----------:| | |||||||||||||||||||||||||||||||||||||||||||||||||||
| | foreach_static_readonly_array (.NET 9) | 153.4 ns | 1.00 | - | | |||||||||||||||||||||||||||||||||||||||||||||||||||
| | foreach_static_readonly_array_via_interface (.NET 10) | 295.7 ns | 1.93 | - | | |||||||||||||||||||||||||||||||||||||||||||||||||||
| ``` | |||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
|||||||||||||||||||||||||||||||||||||||||||||||||||
| Now, let's consider a more challenging example: | |||||||||||||||||||||||||||||||||||||||||||||||||||
| ```csharp | |||||||||||||||||||||||||||||||||||||||||||||||||||
| [MethodImpl(MethodImplOptions.NoInlining)] | |||||||||||||||||||||||||||||||||||||||||||||||||||
| IEnumerable<int> get_opaque_array() => s_ro_array; | |||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
|||||||||||||||||||||||||||||||||||||||||||||||||||
| [Benchmark] | |||||||||||||||||||||||||||||||||||||||||||||||||||
| public int foreach_opaque_array_via_interface() | |||||||||||||||||||||||||||||||||||||||||||||||||||
| { | |||||||||||||||||||||||||||||||||||||||||||||||||||
| IEnumerable<int> o = get_opaque_array(); | |||||||||||||||||||||||||||||||||||||||||||||||||||
| int sum = 0; | |||||||||||||||||||||||||||||||||||||||||||||||||||
| foreach (int i in o) sum += i; | |||||||||||||||||||||||||||||||||||||||||||||||||||
| return sum; | |||||||||||||||||||||||||||||||||||||||||||||||||||
| } | |||||||||||||||||||||||||||||||||||||||||||||||||||
| ``` | |||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
|||||||||||||||||||||||||||||||||||||||||||||||||||
| When compiling `foreach_opaque_array_via_interface`, the JIT does not know the underlying collection type. Fortunately, PGO data can tell the JIT what the likely type of the collection is, and via guarded devirtualization, the JIT can create a fast path under a test for this type. The benefits of PGO are significant, but it isn't enough to reach performance parity with the baseline: | |||||||||||||||||||||||||||||||||||||||||||||||||||
| ``` | |||||||||||||||||||||||||||||||||||||||||||||||||||
| | (.NET 9) Method | Mean | Ratio | Allocated | | |||||||||||||||||||||||||||||||||||||||||||||||||||
| |------------------------------------------------------------- |-----------:|------:|----------:| | |||||||||||||||||||||||||||||||||||||||||||||||||||
| | foreach_static_readonly_array | 153.4 ns | 1.00 | - | | |||||||||||||||||||||||||||||||||||||||||||||||||||
| | foreach_opaque_array_via_interface | 843.2 ns | 5.50 | 32 B | | |||||||||||||||||||||||||||||||||||||||||||||||||||
| | foreach_opaque_array_via_interface (no PGO) | 2,076.4 ns | 13.54 | 32 B | | |||||||||||||||||||||||||||||||||||||||||||||||||||
| ``` | |||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
|||||||||||||||||||||||||||||||||||||||||||||||||||
| Notice how `foreach_opaque_array_via_interface` allocates memory on the heap, suggesting the JIT failed to stack-allocate and promote the enumerator to registers. This is because the JIT relies on a technique called escape analysis to enable stack allocation. Escape analysis determines if an object's lifetime can exceed that of its creation context; if the JIT can guarantee an object will not outlive the current method, it can safely allocate it on the stack. In the above example, calling an interface method on the enumerator to control iteration causes it to escape, as the call takes a reference to the enumerator object. On the fast path of the type test, the JIT can try to devirtualize and inline these interface calls to keep the enumerator from escaping. However, escape analysis typically considers the whole method context, so the slow path's reliance on interface calls prevents the JIT from stack-allocating the enumerator at all. | |||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
|||||||||||||||||||||||||||||||||||||||||||||||||||
| [dotnet/runtime #111473](https://github.com/dotnet/runtime/pull/111473) introduces conditional escape analysis -- a flow-sensitive form of the technique -- to the JIT. Conditional escape analysis can determine if an object will escape only on certain paths through the method, and prompt the JIT to create a fast path where the object never escapes. For array enumeration scenarios, conditional escape analysis reveals the enumerator will escape only when type tests for the collection fail, enabling the JIT to create a copy of the iteration code where the enumerator is stack-allocated and promoted. [benchmark results] | |||||||||||||||||||||||||||||||||||||||||||||||||||
|
|||||||||||||||||||||||||||||||||||||||||||||||||||
| Method | Toolchain | Mean | Median | Ratio | Allocated | Alloc Ratio |
|---|---|---|---|---|---|---|
| foreach_static_readonly_array | net9.0 | 150.78 ns | 150.72 ns | 1.00 | - | NA |
| foreach_static_readonly_array_via_interface_property | net9.0 | 851.75 ns | 849.93 ns | 5.65 | 32 B | NA |
| foreach_opaque_array_via_interface | net9.0 | 874.66 ns | 877.68 ns | 5.80 | 32 B | NA |
| foreach_static_readonly_array | net10.0p2 | 151.75 ns | 151.14 ns | 1.01 | - | NA |
| foreach_static_readonly_array_via_interface_property | net10.0p2 | 280.04 ns | 278.77 ns | 1.86 | - | NA |
| foreach_opaque_array_via_interface | net10.0p2 | 277.89 ns | 277.16 ns | 1.84 | - | NA |
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.
Thank you both, I'll update the tables
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.
For Non-PGO you can check the benchmark numbers here: dotnet/runtime#111948 (comment)
It was benchmarked without tiering compilation to demonstrate how late devirt inlining improvements interact with array de-abstraction.
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.
If I understand correctly, it is usually devirt that enables inlining. In this case, it is the opposite, that we need inlining to enable devirt, which may allow further inlining. Fair? If so, this example is great to talk to.
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.
You're correct, the two tend to create virtuous cycles out of each other.
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.
Should this say Preview 2? Or is this a Preview 1 feature we didn't call out before, or something being improved on furth in preview 2? This is confusing 😕
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.
It's this case. I'll add something here to transition into Preview 2-specific work.