{"id":60786,"date":"2026-09-15T05:00:00","date_gmt":"2026-09-15T12:00:00","guid":{"rendered":"https:\/\/devblogs.microsoft.com\/dotnet\/?p=60786"},"modified":"2026-09-14T21:06:16","modified_gmt":"2026-09-15T04:06:16","slug":"performance-improvements-in-net-11","status":"publish","type":"post","link":"https:\/\/devblogs.microsoft.com\/dotnet\/performance-improvements-in-net-11\/","title":{"rendered":"Performance Improvements in .NET 11"},"content":{"rendered":"<p>Before television shows like <em>The Office<\/em> and <em>Parks and Recreation<\/em> cemented the mockumentary in the minds of millions, there was Christopher Guest. He didn&#8217;t invent the genre, but he&#8217;s widely recognized as one of its most influential practitioners, and for my money, there&#8217;s none better. I&#8217;ve watched <em>Waiting for Guffman<\/em> and <em>Best in Show<\/em> more times than I can count. But the one that has stuck with me the most, the one I quote at the slightest provocation, is <em>This Is Spinal Tap<\/em>.<\/p>\n<p>If you&#8217;ve seen it you already know where this is going (and if you haven&#8217;t, you now have weekend plans). The film is a fictional documentary about an aging English rock band named Spinal Tap, whose members are everything we picture when we picture over-the-top rock stars. In one of its more memorable scenes, the guitarist (Nigel) gives the filmmaker (Marty) a tour of his most prized gear, in particular showing off an amplifier unlike any other: its dials don&#8217;t stop at ten. That leads to what might be the single most quoted exchange in the entire movie:<\/p>\n<blockquote><p><strong>Nigel:<\/strong> &#8220;You see, most blokes, you know, will be playing at ten. You&#8217;re on ten here, all the way up, all the way up, all the way up, you&#8217;re on ten on your guitar. Where can you go from there? Where?&#8221;<\/p>\n<p><strong>Marty:<\/strong> &#8220;I don&#8217;t know.&#8221;<\/p>\n<p><strong>Nigel:<\/strong> &#8220;Nowhere. Exactly. What we do is, if we need that extra push over the cliff, you know what we do?&#8221;<\/p>\n<p><strong>Marty:<\/strong> &#8220;Put it up to eleven?&#8221;<\/p>\n<p><strong>Nigel:<\/strong> &#8220;Eleven. Exactly. One louder.&#8221;<\/p><\/blockquote>\n<p>This is .NET 11. It&#8217;s one louder, with another year&#8217;s worth of performance work\nhaving gone into making the runtime and libraries that much faster. Of course, the premise of Nigel&#8217;s special amplifier is ludicrous, as is exemplified in the subsequent few lines of dialog:<\/p>\n<blockquote><p><strong>Marty:<\/strong> &#8220;Why don&#8217;t you just make ten louder and make ten be the top number and make that a little louder?&#8221;<\/p>\n<p><strong>Nigel:<\/strong> (pauses) &#8220;&#8230;these go to eleven.&#8221;<\/p><\/blockquote>\n<p>In contrast, .NET 11 is actually one higher, one louder. The sections that follow are full of real improvements. A bounds check removed, an allocation that no longer happens, a lock that isn&#8217;t taken, a loop that runs in fewer cycles than it did a year ago, a comparison folded to a constant here, a redundant check hoisted out of a loop there, a couple of instructions fused into one, a syscall sidestepped, an array copy handed off to SIMD, and on and on. That&#8217;s how real performance work goes, accumulating gain after gain, each compounding on the last, until the whole thing is measurably, provably louder. And so, in this post, as I&#8217;ve done in past years with <a href=\"https:\/\/devblogs.microsoft.com\/dotnet\/performance-improvements-in-net-10\/\">.NET 10<\/a>, <a href=\"https:\/\/devblogs.microsoft.com\/dotnet\/performance-improvements-in-net-9\/\">.NET 9<\/a>, <a href=\"https:\/\/devblogs.microsoft.com\/dotnet\/performance-improvements-in-net-8\/\">.NET 8<\/a>, <a href=\"https:\/\/devblogs.microsoft.com\/dotnet\/performance_improvements_in_net_7\/\">.NET 7<\/a>, <a href=\"https:\/\/devblogs.microsoft.com\/dotnet\/performance-improvements-in-net-6\">.NET 6<\/a>, <a href=\"https:\/\/devblogs.microsoft.com\/dotnet\/performance-improvements-in-net-5\">.NET 5<\/a>, <a href=\"https:\/\/devblogs.microsoft.com\/dotnet\/performance-improvements-in-net-core-3-0\">.NET Core 3.0<\/a>, <a href=\"https:\/\/devblogs.microsoft.com\/dotnet\/performance-improvements-in-net-core-2-1\">.NET Core 2.1<\/a>, and <a href=\"https:\/\/devblogs.microsoft.com\/dotnet\/performance-improvements-in-net-core\">.NET Core 2.0<\/a> before it, we&#8217;ll take an unhurried tour through hundreds of them.<\/p>\n<p>This is a long one. It&#8217;s meant to be. Grab your hot beverage of choice, settle in, and let&#8217;s turn it up.<\/p>\n<h2>Benchmarking Setup<\/h2>\n<p>As in previous years, the post is chock full of micro-benchmarks that demonstrate the individual improvements. Almost all of them use <a href=\"https:\/\/www.nuget.org\/packages\/BenchmarkDotNet\">BenchmarkDotNet<\/a>, and each is written to be self-contained so you can try it out yourself.<\/p>\n<p>Start by ensuring you have both <a href=\"https:\/\/dotnet.microsoft.com\/download\/dotnet\/10.0\">.NET 10<\/a> and <a href=\"https:\/\/dotnet.microsoft.com\/download\/dotnet\/11.0\">.NET 11<\/a> installed (most of the benchmarks compare the same code running on both versions) and create a new console project in a fresh <code>benchmarks<\/code> directory:<\/p>\n<pre><code class=\"language-console\">dotnet new console -o benchmarks\r\ncd benchmarks<\/code><\/pre>\n<p>Replace the contents of the generated <code>benchmarks.csproj<\/code> with the following, which multi-targets both versions so that BenchmarkDotNet can build for each:<\/p>\n<pre><code class=\"language-xml\">&lt;Project Sdk=\"Microsoft.NET.Sdk\"&gt;\r\n\r\n  &lt;PropertyGroup&gt;\r\n    &lt;OutputType&gt;Exe&lt;\/OutputType&gt;\r\n    &lt;TargetFrameworks&gt;net11.0;net10.0&lt;\/TargetFrameworks&gt;\r\n    &lt;LangVersion&gt;preview&lt;\/LangVersion&gt;\r\n    &lt;ImplicitUsings&gt;enable&lt;\/ImplicitUsings&gt;\r\n    &lt;Nullable&gt;enable&lt;\/Nullable&gt;\r\n    &lt;AllowUnsafeBlocks&gt;true&lt;\/AllowUnsafeBlocks&gt;\r\n    &lt;ServerGarbageCollection&gt;true&lt;\/ServerGarbageCollection&gt;\r\n    &lt;SystemPackageVersion Condition=\"'$(TargetFramework)' == 'net10.0'\"&gt;10.0.12&lt;\/SystemPackageVersion&gt;\r\n    &lt;SystemPackageVersion Condition=\"'$(TargetFramework)' == 'net11.0'\"&gt;11.0.0-rc.1.26425.128&lt;\/SystemPackageVersion&gt;\r\n  &lt;\/PropertyGroup&gt;\r\n\r\n  &lt;ItemGroup&gt;\r\n    &lt;PackageReference Include=\"BenchmarkDotNet\" Version=\"0.16.0-preview.1\" \/&gt;\r\n    &lt;PackageReference Include=\"System.IO.Hashing\" Version=\"$(SystemPackageVersion)\" \/&gt;\r\n    &lt;PackageReference Include=\"System.Runtime.Caching\" Version=\"$(SystemPackageVersion)\" \/&gt;\r\n    &lt;PackageReference Include=\"System.Numerics.Tensors\" Version=\"$(SystemPackageVersion)\" \/&gt;\r\n  &lt;\/ItemGroup&gt;\r\n\r\n&lt;\/Project&gt;<\/code><\/pre>\n<p>For a given benchmark to test, copy its complete contents over everything in <code>Program.cs<\/code> and then run it. Each benchmark includes as a comment at the top the exact command to use. In most cases, it&#8217;s:<\/p>\n<pre><code class=\"language-console\">dotnet run -c Release -f net10.0 --filter \"*\" --runtimes net10.0 net11.0<\/code><\/pre>\n<p>which builds in Release and runs the benchmark against both .NET 10 and .NET 11, emitting a side-by-side comparison. The other common form, used when a benchmark is comparing two coding approaches on a single runtime (rather than the same code across two runtimes) is:<\/p>\n<pre><code class=\"language-console\">dotnet run -c Release -f net11.0 --filter \"*\"<\/code><\/pre>\n<p>The usual disclaimer applies: these are micro-benchmarks, many measuring operations so short that a blink would miss them. Your results will vary with your hardware, OS, runtime configuration, what else your machine happens to be doing at that exact moment, and whether Mercury is in retrograde.<\/p>\n<p>Every line of managed code ultimately ends up at the just-in-time compiler, so let&#8217;s start there.<\/p>\n<h2>JIT<\/h2>\n<p>Of all the places to improve .NET&#8217;s performance, few have as broad an impact as the just-in-time (JIT) compiler. C#, F#, and Visual Basic are typically compiled first to intermediate language (IL), and the JIT ultimately turns that IL into the native instructions the CPU executes. A JIT improvement can therefore benefit application and library code wherever the optimized pattern occurs, often with no source changes or recompilation of the application itself. Even removing a single instruction or proving one check unnecessary can add up when the code is on a very hot path.<\/p>\n<h3>Deabstraction<\/h3>\n<p>We as developers love our abstractions. They let us write clean, reusable, object-oriented code, but we don&#8217;t want to pay for every abstraction at run time. The runtime can often undo an abstraction when it proves the effects aren&#8217;t observable. It can look at a virtual call and determine which concrete method it&#8217;ll invoke, look at a heap allocation and recognize that the object never leaves the current stack frame, or look at an interface cast and reuse a type fact already established earlier in the method. This process is called &#8220;deabstraction.&#8221; .NET has improved steadily in this area for years, and that continues in .NET 11.<\/p>\n<p>Every time you write <code>interface<\/code> in C#, you&#8217;re creating a contract, a promise that any type implementing that interface can be substituted for any other. That flexibility is enormously valuable because, for example, it&#8217;s what lets us write <code>IEnumerable&lt;T&gt;<\/code> and have it work equally well over arrays, lists, other collections, LINQ, custom iterators, and so on. But the CPU doesn&#8217;t know anything about these contracts; it just knows how to execute instructions. Turning &#8220;call whatever method this interface reference points to&#8221; into actual machine instructions requires special machinery. Consider this example:<\/p>\n<pre><code class=\"language-csharp\">\/\/ dotnet run -c Release -f net11.0 --filter \"*\"\r\n\r\nusing BenchmarkDotNet.Attributes;\r\nusing BenchmarkDotNet.Running;\r\nusing System.Runtime.CompilerServices;\r\n\r\nBenchmarkSwitcher.FromAssembly(typeof(Benchmarks).Assembly).Run(args);\r\n\r\n[DisassemblyDiagnoser, HideColumns(\"Job\", \"Error\", \"StdDev\", \"Median\", \"RatioSD\")]\r\npublic class Benchmarks\r\n{\r\n    private Animal _animal = Environment.TickCount &gt;= 0 ? new Dog() : new Cat();\r\n\r\n    [Benchmark]\r\n    public int Speak() =&gt; _animal.Speak();\r\n\r\n    public abstract class Animal\r\n    {\r\n        public abstract int Speak();\r\n    }\r\n\r\n    private sealed class Dog : Animal\r\n    {\r\n        [MethodImpl(MethodImplOptions.NoInlining)]\r\n        public override int Speak() =&gt; 1;\r\n    }\r\n\r\n    private sealed class Cat : Animal\r\n    {\r\n        [MethodImpl(MethodImplOptions.NoInlining)]\r\n        public override int Speak() =&gt; 2;\r\n    }\r\n}<\/code><\/pre>\n<p>At compile time, all else equal, the JIT doesn&#8217;t know whether <code>_animal<\/code> is a <code>Dog<\/code> or a <code>Cat<\/code>. It generates code that loads the instance&#8217;s &#8220;method table pointer&#8221; (its object type handle), sometimes called a &#8220;vtable pointer&#8221;, stored at the beginning of every .NET object, indexes into the method table at the known slot for <code>Speak<\/code>, and calls the function pointer found there:<\/p>\n<pre><code class=\"language-asm\">; x64\r\nmov     rcx, [rcx+8]   ; load _animal\r\nmov     rax, [rcx]     ; load method table\r\nmov     rax, [rax+40]  ; load vtable chunk\r\ncall    qword ptr [rax+20]<\/code><\/pre>\n<p>For this one call to <code>Speak<\/code>, we pay three dependent memory dereferences and an indirect call because the processor doesn&#8217;t know for certain in advance where the call is going (it might guess, or &#8220;speculatively execute&#8221;, but it has to be prepared for the possibility it was wrong), and because the call target is indirect, the JIT can&#8217;t inline the callee. Whatever <code>Speak<\/code> does, its code can&#8217;t be folded into the calling method.<\/p>\n<p>That&#8217;s a performance problem. Those indirections have overhead, but the bigger cost is the lost opportunity to inline. Inlining not only saves function call overhead, more importantly it opens the callee&#8217;s code up to the same optimizations that are operating on the caller, such as constant propagation, dead code elimination, bounds check elimination, further devirtualization, etc. That means a series of small virtual calls that each look innocent can, when devirtualized and inlined, collapse into a handful of instructions that would be unrecognizable and way cheaper when compared to the original source code. Without inlining, each callee is an opaque box; with it, the JIT can see through the layers.<\/p>\n<p>We as .NET developers constantly rely on the JIT&#8217;s sophisticated heuristics for inlining that weigh the IL size of the callee, the exact work the callee is performing, the call frequency of the method, the expected benefit from constant arguments, and dozens of other factors. For virtual calls, the JIT needs to know what the actual target of the call will be; it needs to &#8220;devirtualize&#8221;. In some cases, it can determine that statically, where it has exact-type knowledge. For example, if the JIT can prove that <code>animal<\/code> is always a <code>Dog<\/code>, whether because it was just allocated with <code>new Dog()<\/code>:<\/p>\n<pre><code class=\"language-csharp\">Animal animal = GetSomeAnimal();\r\nanimal.Speak();\r\n...\r\nstatic Animal GetSomeAnimal() =&gt; new Dog(); \/\/ inlineable<\/code><\/pre>\n<p>or because the variable&#8217;s type is a sealed class:<\/p>\n<pre><code class=\"language-csharp\">Dog animal = GetSomeAnimal();\r\nanimal.Speak();\r\n...\r\nsealed class Dog { ... } \/\/ impossible for `animal` to be anything other than a `Dog`<\/code><\/pre>\n<p>or with NativeAOT and whole-program compilation, if it sees that <code>Animal<\/code> is abstract and the only type in the whole application that derives from <code>Animal<\/code> is <code>Dog<\/code>:<\/p>\n<pre><code class=\"language-csharp\">Animal animal = GetSomeAnimal();\r\nanimal.Speak();\r\n...\r\nabstract class Animal { ... }\r\nclass Dog : Animal { ... } \/\/ no other such derived type<\/code><\/pre>\n<p>or other such validation, it can emit a call to <code>Dog.Speak()<\/code> directly, and the inliner can take its shot.<\/p>\n<p>But for other cases where it can&#8217;t prove this with static analysis, the JIT turns to profile-guided optimization (PGO). PGO sounds fancy, but it&#8217;s conceptually simple. With &#8220;tiered compilation&#8221;, when a method is first invoked, it can be compiled &#8220;just in time&#8221; with few-to-no optimizations (this is referred to as Tier 0). The JIT can include in this compilation additional probes (think &#8220;printf debugging&#8221;) that let it track a bunch of interesting information about the nature of the code, recording what actually happens when it runs: which branches are taken, what are the concrete types that show up at virtual call sites or cast attempts, and so on. If the method is invoked enough or loops enough times, the runtime can ask the JIT to produce a new optimized version (referred to as Tier 1). That compilation can then factor in all of the learnings gathered as part of that profiling.<\/p>\n<p>The JIT, of course, still needs to generate code that&#8217;s always correct. Even if a dynamic profile says <code>animal<\/code> was <code>Dog<\/code> 100% of the time, that doesn&#8217;t guarantee it&#8217;ll always be <code>Dog<\/code> in the future; it could be that the first 1000 calls passed in a <code>Dog<\/code> but the 1001st call is going to pass in <code>Dolphin<\/code>. How can the JIT incorporate this learning then? By emitting a run-time check. The <code>Dog<\/code> path can get a direct call, which may then be inlinable, and the other path keeps the original virtual call as the fallback. The speed comes from making the common case tiny, while correctness comes from leaving the uncommon case intact.<\/p>\n<pre><code class=\"language-csharp\">\/\/ Approximately what the JIT generates\r\nif (animal?.GetType() == typeof(Dog))\r\n{\r\n    ((Dog)animal).Speak();  \/\/ devirtualized, inlinable\r\n}\r\nelse\r\n{\r\n    animal.Speak(); \/\/ original virtual call, hopefully rare\r\n}<\/code><\/pre>\n<p>This &#8220;guess and verify&#8221; pattern, called &#8220;guarded devirtualization&#8221; (GDV), accounts for many of the biggest throughput wins in real workloads. It&#8217;s applicable not only to virtual dispatch but also to interface dispatch, which also happens to be a bit more expensive than virtual dispatch because a type can implement any number of interfaces and that means the interface slots don&#8217;t simply map to fixed vtable positions.<\/p>\n<p>Deabstraction can also make object creation more efficient when it reveals what kind of object is involved. In general, objects in .NET are allocated on the garbage collected heap, tracked by the garbage collector (GC), and collected when no longer reachable. Heap allocation is typically fast, often effectively just bumping a pointer. However, when there&#8217;s not enough space available to bump the pointer, it can get much more expensive, including needing to incur a garbage collection. Every allocated object also effectively incurs the amortized cost of all collections, as every allocated object eventually needs to be cleaned up.<\/p>\n<p>&#8220;Escape analysis&#8221; is the compiler technique that lets us ask whether this object ever &#8220;escapes&#8221; the current method. If an object reference to a newly allocated object provably doesn&#8217;t escape, then the JIT can more efficiently allocate it. It needn&#8217;t store it on the GC heap, because nothing could possibly need to reference that object again, so it can instead allocate the object on the stack, making both allocation and cleanup essentially free. Stack allocation is even faster than heap bump-pointer allocation; it&#8217;s just decrementing the stack pointer, which is typically already in a register. And more importantly it means zero GC impact, because the stack frame is freed atomically on function return.<\/p>\n<p>The JIT&#8217;s been progressively expanding escape analysis over the past several .NET releases, with .NET 9 and 10 seeing significant investments in stack-allocating delegates and closures, <code>Nullable&lt;T&gt;<\/code> temporaries, and small helper objects. The key theme is that every false positive escape, every time the JIT incorrectly concludes an object may escape when it really doesn&#8217;t, represents a heap allocation that could have been avoided, and we want to whittle away at that false positive list. In .NET 11, the JIT trims that list in several ways.<\/p>\n<p>We&#8217;ll start with nullable boxing. Consider this benchmark:<\/p>\n<pre><code class=\"language-csharp\">\/\/ dotnet run -c Release -f net10.0 --filter \"*\" --runtimes net10.0 net11.0\r\n\r\nusing BenchmarkDotNet.Attributes;\r\nusing BenchmarkDotNet.Running;\r\n\r\nBenchmarkSwitcher.FromAssembly(typeof(Benchmarks).Assembly).Run(args);\r\n\r\n[MemoryDiagnoser(false), HideColumns(\"Job\", \"Error\", \"StdDev\", \"Median\", \"RatioSD\")]\r\npublic class Benchmarks\r\n{\r\n    private int? _nullableNull;\r\n    private int? _nullableValue = 42;\r\n\r\n    [Benchmark]\r\n    public object? BoxNullableNull() =&gt; (object?)_nullableNull;\r\n\r\n    [Benchmark]\r\n    public object? BoxNullableValue() =&gt; (object?)_nullableValue;\r\n\r\n    [Benchmark]\r\n    public string? FormatNullableInt() =&gt; Format(_nullableValue);\r\n\r\n    private static string? Format&lt;T&gt;(T value)\r\n    {\r\n        if (value is IFormattable formattable)\r\n            return formattable.ToString(null, null);\r\n\r\n        return null;\r\n    }\r\n}<\/code><\/pre>\n<table>\n<thead>\n<tr>\n<th>Method<\/th>\n<th>Runtime<\/th>\n<th>Mean<\/th>\n<th>Ratio<\/th>\n<th>Allocated<\/th>\n<th>Alloc Ratio<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td>BoxNullableNull<\/td>\n<td>.NET 10.0<\/td>\n<td>2.095 ns<\/td>\n<td>1.00<\/td>\n<td>&#8211;<\/td>\n<td>&#8211;<\/td>\n<\/tr>\n<tr>\n<td>BoxNullableNull<\/td>\n<td>.NET 11.0<\/td>\n<td>1.764 ns<\/td>\n<td>0.84<\/td>\n<td>&#8211;<\/td>\n<td>&#8211;<\/td>\n<\/tr>\n<tr>\n<td>BoxNullableValue<\/td>\n<td>.NET 10.0<\/td>\n<td>9.213 ns<\/td>\n<td>1.00<\/td>\n<td>24 B<\/td>\n<td>1.00<\/td>\n<\/tr>\n<tr>\n<td>BoxNullableValue<\/td>\n<td>.NET 11.0<\/td>\n<td>4.126 ns<\/td>\n<td>0.45<\/td>\n<td>24 B<\/td>\n<td>1.00<\/td>\n<\/tr>\n<tr>\n<td>FormatNullableInt<\/td>\n<td>.NET 10.0<\/td>\n<td>9.583 ns<\/td>\n<td>1.00<\/td>\n<td>24 B<\/td>\n<td>1.00<\/td>\n<\/tr>\n<tr>\n<td>FormatNullableInt<\/td>\n<td>.NET 11.0<\/td>\n<td>1.987 ns<\/td>\n<td>0.21<\/td>\n<td>&#8211;<\/td>\n<td>0<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<p><a href=\"https:\/\/github.com\/dotnet\/runtime\/pull\/122167\">dotnet\/runtime#122167<\/a> expands nullable boxing inside the JIT, exposing the temporary box to escape analysis; previously, a runtime helper hid it. For a <code>null<\/code> input, there&#8217;s no allocation on either version, because nothing gets boxed. And on both versions, <code>BoxNullableValue<\/code> returns the boxed object, meaning the object escapes, so the 24-byte allocation remains. However, for <code>FormatNullableInt<\/code>, the JIT in .NET 11 can now see that the temporary 24-byte box doesn&#8217;t escape and eliminates that heap allocation entirely.<\/p>\n<p>Escape analysis improved further for enumerators, through a mechanism called Conditional Escape Analysis (CEA). Support for CEA was introduced in .NET 10, but .NET 11 extends the set of patterns that this analysis can safely recognize. The existing escape analysis asks whether a reference created by an allocation can flow somewhere the JIT can no longer track, such as an unknown call. If it can, the object must remain on the heap. That analysis is necessarily conservative and largely flow-insensitive: if an object might be passed to an interface call on any path, it doesn&#8217;t try to prove that the path containing that call is mutually exclusive with the path containing the allocation.<\/p>\n<p>Unfortunately, that&#8217;s exactly what GDV produces when it optimizes a <code>foreach<\/code> over an <code>IEnumerable&lt;T&gt;<\/code>. As noted earlier, GDV turns an interface call into a type check with two branches: a fast branch for the likely collection type and a fallback branch containing the original interface call. Devirtualization and inlining along the fast branch will often reveal an enumerator allocation for the collection type, while later enumerator guards retain fallback calls such as <code>IEnumerator&lt;T&gt;.MoveNext<\/code>. The existing analysis sees those calls and concludes that the locally allocated enumerator might escape. CEA instead records the relationship between the fast-path allocation and the enumerator local tested by the later guards. If every apparent escape occurs only behind a failed type check, the JIT can clone the region into a hot version where those checks are known to succeed. In that clone, the object can&#8217;t reach the fallback calls, so it can be stack-allocated and often promoted into separate scalar locals. The original region remains as the general slow path.<\/p>\n<p>One case .NET 10 didn&#8217;t handle, though, was a <code>GetEnumerator()<\/code> implementation that returns the result of another <code>GetEnumerator()<\/code> call. A collection expression converted to <code>IEnumerable&lt;int&gt;<\/code>, for example, uses a compiler-generated read-only-array wrapper with exactly this structure: the wrapper&#8217;s <code>GetEnumerator()<\/code> delegates to the underlying array&#8217;s <code>GetEnumerator<\/code>. With <a href=\"https:\/\/github.com\/dotnet\/runtime\/pull\/122946\">dotnet\/runtime#122946<\/a>, the JIT in .NET 11 handles this &#8220;chaining&#8221;:<\/p>\n<pre><code class=\"language-csharp\">\/\/ dotnet run -c Release -f net10.0 --filter \"*\" --runtimes net10.0 net11.0\r\n\r\nusing BenchmarkDotNet.Attributes;\r\nusing BenchmarkDotNet.Running;\r\n\r\nBenchmarkSwitcher.FromAssembly(typeof(Benchmarks).Assembly).Run(args);\r\n\r\n[MemoryDiagnoser(false), HideColumns(\"Job\", \"Error\", \"StdDev\", \"Median\", \"RatioSD\")]\r\npublic class Benchmarks\r\n{\r\n    private static readonly IEnumerable&lt;int&gt; s_readOnlyStatic = [1, 2, 3, 4, 5];\r\n    private readonly IEnumerable&lt;int&gt; _readOnlyInstance = [1, 2, 3, 4, 5];\r\n\r\n    [Benchmark]\r\n    public int ReadOnlyStatic()\r\n    {\r\n        int sum = 0;\r\n        foreach (int item in s_readOnlyStatic) sum += item;\r\n        return sum;\r\n    }\r\n\r\n    [Benchmark]\r\n    public int ReadOnlyInstance()\r\n    {\r\n        int sum = 0;\r\n        foreach (int item in _readOnlyInstance) sum += item;\r\n        return sum;\r\n    }\r\n}<\/code><\/pre>\n<table>\n<thead>\n<tr>\n<th>Method<\/th>\n<th>Runtime<\/th>\n<th>Mean<\/th>\n<th>Ratio<\/th>\n<th>Allocated<\/th>\n<th>Alloc Ratio<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td>ReadOnlyStatic<\/td>\n<td>.NET 10.0<\/td>\n<td>2.665 ns<\/td>\n<td>1.00<\/td>\n<td>&#8211;<\/td>\n<td>&#8211;<\/td>\n<\/tr>\n<tr>\n<td>ReadOnlyStatic<\/td>\n<td>.NET 11.0<\/td>\n<td>2.666 ns<\/td>\n<td>1.00<\/td>\n<td>&#8211;<\/td>\n<td>&#8211;<\/td>\n<\/tr>\n<tr>\n<td>ReadOnlyInstance<\/td>\n<td>.NET 10.0<\/td>\n<td>13.874 ns<\/td>\n<td>1.00<\/td>\n<td>32 B<\/td>\n<td>1.00<\/td>\n<\/tr>\n<tr>\n<td>ReadOnlyInstance<\/td>\n<td>.NET 11.0<\/td>\n<td>2.674 ns<\/td>\n<td>0.19<\/td>\n<td>&#8211;<\/td>\n<td>0<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<p><code>ReadOnlyStatic<\/code>, whose <code>static readonly<\/code> field the JIT can effectively treat as a constant, was already optimized in .NET 10. In .NET 11, the instance-field case also loses its 32-byte enumerator allocation and converges on the same throughput.<\/p>\n<p><a href=\"https:\/\/github.com\/dotnet\/runtime\/pull\/121918\">dotnet\/runtime#121918<\/a> from <a href=\"https:\/\/github.com\/MichalPetryka\">@MichalPetryka<\/a> fixes another way an address could unnecessarily make an object appear to escape. The IL <code>constrained.<\/code> prefix lets one generic <code>callvirt<\/code> sequence work for both value types and reference types: it can avoid boxing a value type, while for a reference type it dereferences the receiver and performs normal virtual dispatch. <code>ObjectEqualityComparer&lt;T&gt;.Equals<\/code>, used in the following benchmark by <code>EqualityComparer&lt;T&gt;.Default<\/code>, contains such a call to <code>value.Equals(other)<\/code>. The receiver was represented as an indirect read through the address of a local. Merely taking that address marked the local as exposed, preventing the newly allocated <code>Value<\/code> from being considered for stack allocation. The receiver is now represented as a direct value load instead, and the 24-byte heap allocation disappears.<\/p>\n<pre><code class=\"language-csharp\">\/\/ dotnet run -c Release -f net10.0 --filter \"*\" --runtimes net10.0 net11.0\r\n\r\nusing BenchmarkDotNet.Attributes;\r\nusing BenchmarkDotNet.Running;\r\nusing System.Collections.Generic;\r\n\r\nBenchmarkSwitcher.FromAssembly(typeof(Benchmarks).Assembly).Run(args);\r\n\r\n[MemoryDiagnoser(false), HideColumns(\"Job\", \"Error\", \"StdDev\", \"Median\", \"RatioSD\")]\r\npublic class Benchmarks\r\n{\r\n    private static readonly Value s_other = new(42);\r\n\r\n    [Benchmark]\r\n    public bool Equals() =&gt; EqualityComparer&lt;Value&gt;.Default.Equals(new Value(42), s_other);\r\n\r\n    private sealed class Value(int value)\r\n    {\r\n        private readonly int _value = value;\r\n\r\n        public override bool Equals(object? obj) =&gt; obj is Value other &amp;&amp; _value == other._value;\r\n\r\n        public override int GetHashCode() =&gt; _value;\r\n    }\r\n}<\/code><\/pre>\n<table>\n<thead>\n<tr>\n<th>Method<\/th>\n<th>Runtime<\/th>\n<th>Mean<\/th>\n<th>Ratio<\/th>\n<th>Allocated<\/th>\n<th>Alloc Ratio<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td>Equals<\/td>\n<td>.NET 10.0<\/td>\n<td>3.874 ns<\/td>\n<td>1.00<\/td>\n<td>24 B<\/td>\n<td>1.00<\/td>\n<\/tr>\n<tr>\n<td>Equals<\/td>\n<td>.NET 11.0<\/td>\n<td>1.786 ns<\/td>\n<td>0.46<\/td>\n<td>&#8211;<\/td>\n<td>0<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<p>While CEA can move a non-escaping object off the GC heap, sometimes the JIT can go further and prove an allocation need not exist at all. Generic code provides a common source of such opportunities through boxing. For example, the <code>ArgumentNullException.ThrowIfNull<\/code> method accepts an <code>object value<\/code>. That means when you have a method like this:<\/p>\n<pre><code class=\"language-csharp\">static void Test&lt;T&gt;(T value)\r\n{\r\n    ArgumentNullException.ThrowIfNull(value);\r\n    ...\r\n}<\/code><\/pre>\n<p>when <code>T<\/code> is constrained to a non-nullable struct, boxing is incurred, in order to pass <code>value<\/code> as <code>object<\/code>. <code>ThrowIfNull<\/code> here is a nop if <code>value<\/code> is non-<code>null<\/code> (since the method is simply <code>if (value is null) Throw();<\/code>), and previous releases successfully optimized away that boxing in optimized code. However, in Tier 0, that optimization wasn&#8217;t applied, and <code>ThrowIfNull<\/code> would end up allocating. While this wouldn&#8217;t negatively impact steady-state throughput, it would lead to annoying noise in profiling, as well as additional overhead during startup, where such use wasn&#8217;t yet promoted out of Tier 0. In .NET 11, <a href=\"https:\/\/github.com\/dotnet\/runtime\/pull\/129392\">dotnet\/runtime#129392<\/a> adds support for this in Tier 0 as well.<\/p>\n<p>On the virtual-dispatch side, multiple PRs contribute to improving generic virtual methods (GVMs). <a href=\"https:\/\/github.com\/dotnet\/runtime\/pull\/120866\">dotnet\/runtime#120866<\/a> from <a href=\"https:\/\/github.com\/hez2010\">@hez2010<\/a> stops eagerly spilling <code>ldvirtftn<\/code> call targets into a temporary, and lets generic virtual target resolution move ahead of argument setup when legal. <a href=\"https:\/\/github.com\/dotnet\/runtime\/pull\/122023\">dotnet\/runtime#122023<\/a> from <a href=\"https:\/\/github.com\/hez2010\">@hez2010<\/a> then enables the JIT to devirtualize non-shared GVMs, carrying the generic context needed to turn the indirect dispatch into a direct, and potentially inlineable, call. And <a href=\"https:\/\/github.com\/dotnet\/runtime\/pull\/128702\">dotnet\/runtime#128702<\/a> from <a href=\"https:\/\/github.com\/hez2010\">@hez2010<\/a> extends that support to shared GVMs and default interface implementations that require an instantiating stub. These optimizations can increase total code size when the newly direct calls are inlined, but that&#8217;s generally the desired trade: more of the actual work becomes visible to the optimizer. Consider the following benchmark:<\/p>\n<pre><code class=\"language-csharp\">\/\/ dotnet run -c Release -f net10.0 --filter \"*\" --runtimes net10.0 net11.0\r\n\r\nusing BenchmarkDotNet.Attributes;\r\nusing BenchmarkDotNet.Running;\r\nusing System.Runtime.CompilerServices;\r\n\r\nBenchmarkSwitcher.FromAssembly(typeof(Benchmarks).Assembly).Run(args);\r\n\r\n[MemoryDiagnoser(false), HideColumns(\"Job\", \"Error\", \"StdDev\", \"Median\", \"RatioSD\")]\r\npublic class Benchmarks\r\n{\r\n    [Benchmark]\r\n    public int NonShared() =&gt; ((IProcessor)new Processor()).SizeOf(42);\r\n\r\n    [Benchmark]\r\n    public int Shared() =&gt; ((IProcessor)new Processor()).SizeOf(\"hello\");\r\n\r\n    private interface IProcessor\r\n    {\r\n        int SizeOf&lt;T&gt;(T value);\r\n    }\r\n\r\n    private sealed class Processor : IProcessor\r\n    {\r\n        public int SizeOf&lt;T&gt;(T value) =&gt; Unsafe.SizeOf&lt;T&gt;();\r\n    }\r\n}<\/code><\/pre>\n<p>Casting a freshly allocated <code>Processor<\/code> to <code>IProcessor<\/code> incurs an interface generic virtual call in the IL, but the JIT is now able to see the receiver&#8217;s exact type, even in the shared <code>string<\/code> case, such that .NET 11 devirtualizes and inlines both calls. That in turn exposes <code>Unsafe.SizeOf&lt;T&gt;()<\/code> as a constant and proves that the short-lived <code>Processor<\/code> doesn&#8217;t need to be allocated at all.<\/p>\n<table>\n<thead>\n<tr>\n<th>Method<\/th>\n<th>Runtime<\/th>\n<th>Mean<\/th>\n<th>Ratio<\/th>\n<th>Allocated<\/th>\n<th>Alloc Ratio<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td>NonShared<\/td>\n<td>.NET 10.0<\/td>\n<td>6.678 ns<\/td>\n<td>1.00<\/td>\n<td>24 B<\/td>\n<td>1.00<\/td>\n<\/tr>\n<tr>\n<td>NonShared<\/td>\n<td>.NET 11.0<\/td>\n<td>1.764 ns<\/td>\n<td>0.26<\/td>\n<td>&#8211;<\/td>\n<td>0<\/td>\n<\/tr>\n<tr>\n<td>Shared<\/td>\n<td>.NET 10.0<\/td>\n<td>7.166 ns<\/td>\n<td>1.00<\/td>\n<td>24 B<\/td>\n<td>1.00<\/td>\n<\/tr>\n<tr>\n<td>Shared<\/td>\n<td>.NET 11.0<\/td>\n<td>1.764 ns<\/td>\n<td>0.25<\/td>\n<td>&#8211;<\/td>\n<td>0<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<p>Building on that, <a href=\"https:\/\/github.com\/dotnet\/runtime\/pull\/123183\">dotnet\/runtime#123183<\/a> from <a href=\"https:\/\/github.com\/hez2010\">@hez2010<\/a> enables ReadyToRun compilation to resolve and devirtualize more non-shared generic virtual calls that would otherwise remain indirect, and <a href=\"https:\/\/github.com\/dotnet\/runtime\/pull\/130202\">dotnet\/runtime#130202<\/a> from <a href=\"https:\/\/github.com\/hez2010\">@hez2010<\/a> extends that support to NativeAOT. NativeAOT represents some generic virtual targets as &#8220;fat pointers&#8221; (pointers that are more than just an address, typically an address and associated metadata, and that in this case carry both a code address and generic context); by deferring that transformation until after exact-type devirtualization has had a chance to run, the JIT can turn an interface call site with a single known target to a non-shared GVM into a direct call that may then be inlined.<\/p>\n<p>Type information also needs to survive the transformations the JIT performs internally. If the JIT spills a reference expression into a temporary while restructuring a tree, losing the expression&#8217;s exact class information can turn a call that was devirtualizable back into an opaque virtual call. That&#8217;s what happens here in .NET 10: <code>Value<\/code> gets boxed and <code>SetValue<\/code> is invoked through <code>IValue<\/code>. <a href=\"https:\/\/github.com\/dotnet\/runtime\/pull\/128485\">dotnet\/runtime#128485<\/a> from <a href=\"https:\/\/github.com\/hez2010\">@hez2010<\/a> preserves the class handle and exactness on the temporary. With that information still available, .NET 11 devirtualizes and inlines the call, eliminating the box and its 24-byte allocation.<\/p>\n<p>Separately, <a href=\"https:\/\/github.com\/dotnet\/runtime\/pull\/127433\">dotnet\/runtime#127433<\/a> relaxes the inliner&#8217;s budget heuristics for callees on <code>[Intrinsic]<\/code> types like <code>Span<\/code> and <code>Vector<\/code>. These types intentionally expose many small, composable methods that serve as gateways to JIT-recognized operations. If a wrapper remains as a call, the caller pays the call overhead and optimizations around it see an opaque boundary. If it inlines, the importer can replace its body with an intrinsic node and optimize that node together with the surrounding indexing, bounds checks, and vector operations. Giving such wrappers more favorable budgeting therefore keeps more of them inlineable and exposes more of the actual operation to the rest of the optimizer.<\/p>\n<p>One of the core abstraction-enabling mechanisms in .NET is delegates: they let us pass around objects representing functions to be invoked, carrying with them associated required state. Deabstraction enables avoiding paying for the overheads associated with delegates in some cases. For the rest, we still want those delegates to be as cheap as possible. <a href=\"https:\/\/github.com\/dotnet\/runtime\/pull\/99200\">dotnet\/runtime#99200<\/a> from\n<a href=\"https:\/\/github.com\/MichalPetryka\">@MichalPetryka<\/a> simplifies CoreCLR&#8217;s delegate\nrepresentation, removing one pointer-sized field from every delegate object.\nThat saves 8 bytes per delegate in a 64-bit CoreCLR process.\n<a href=\"https:\/\/github.com\/dotnet\/runtime\/pull\/129304\">dotnet\/runtime#129304<\/a> from\n<a href=\"https:\/\/github.com\/MichalPetryka\">@MichalPetryka<\/a> improves Native AOT&#8217;s\ndelegate layout separately by reordering its existing four fields so related\nvalues are adjacent. The updated layouts also give equality and hash-code\noperations more direct access to the method identity they need.<\/p>\n<pre><code class=\"language-csharp\">\/\/ dotnet run -c Release -f net10.0 --filter \"*\" --runtimes net10.0 net11.0\r\n\r\nusing BenchmarkDotNet.Attributes;\r\nusing BenchmarkDotNet.Running;\r\n\r\nBenchmarkSwitcher.FromAssembly(typeof(Benchmarks).Assembly).Run(args);\r\n\r\n[MemoryDiagnoser(false), HideColumns(\"Job\", \"Error\", \"StdDev\", \"Median\", \"RatioSD\")]\r\npublic class Benchmarks\r\n{\r\n    private static readonly Target s_target = new();\r\n    private static readonly Func&lt;int&gt; s_first = s_target.GetValue;\r\n    private static readonly Func&lt;int&gt; s_second = s_target.GetValue;\r\n\r\n    [Benchmark]\r\n    public Func&lt;int&gt; ClosedInstance() =&gt; s_target.GetValue;\r\n\r\n    [Benchmark]\r\n    public bool DelegateEquals() =&gt; s_first.Equals(s_second);\r\n\r\n    [Benchmark]\r\n    public int DelegateGetHashCode() =&gt; s_first.GetHashCode();\r\n\r\n    private sealed class Target\r\n    {\r\n        public int GetValue() =&gt; 42;\r\n    }\r\n}<\/code><\/pre>\n<table>\n<thead>\n<tr>\n<th>Method<\/th>\n<th>Runtime<\/th>\n<th>Mean<\/th>\n<th>Ratio<\/th>\n<th>Allocated<\/th>\n<th>Alloc Ratio<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td>ClosedInstance<\/td>\n<td>.NET 10.0<\/td>\n<td>7.395 ns<\/td>\n<td>1.00<\/td>\n<td>64 B<\/td>\n<td>1.00<\/td>\n<\/tr>\n<tr>\n<td>ClosedInstance<\/td>\n<td>.NET 11.0<\/td>\n<td>6.844 ns<\/td>\n<td>0.93<\/td>\n<td>56 B<\/td>\n<td>0.88<\/td>\n<\/tr>\n<tr>\n<td>DelegateEquals<\/td>\n<td>.NET 10.0<\/td>\n<td>3.254 ns<\/td>\n<td>1.00<\/td>\n<td>&#8211;<\/td>\n<td>&#8211;<\/td>\n<\/tr>\n<tr>\n<td>DelegateEquals<\/td>\n<td>.NET 11.0<\/td>\n<td>2.215 ns<\/td>\n<td>0.68<\/td>\n<td>&#8211;<\/td>\n<td>&#8211;<\/td>\n<\/tr>\n<tr>\n<td>DelegateGetHashCode<\/td>\n<td>.NET 10.0<\/td>\n<td>5.623 ns<\/td>\n<td>1.00<\/td>\n<td>&#8211;<\/td>\n<td>&#8211;<\/td>\n<\/tr>\n<tr>\n<td>DelegateGetHashCode<\/td>\n<td>.NET 11.0<\/td>\n<td>3.741 ns<\/td>\n<td>0.67<\/td>\n<td>&#8211;<\/td>\n<td>&#8211;<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<p><a href=\"https:\/\/github.com\/dotnet\/runtime\/pull\/129410\">dotnet\/runtime#129410<\/a> from\n<a href=\"https:\/\/github.com\/MichalPetryka\">@MichalPetryka<\/a> follows up on the CoreCLR\nlayout by placing the target object and method pointer next to each other.\nThose are commonly consumed together during invocation, and the adjacency\nenables paired loads on architectures such as Arm64.<\/p>\n<h3>Runtime Async<\/h3>\n<p>For more than a decade, <code>async<\/code> and <code>await<\/code> have let us write asynchronous code that looks remarkably similar to synchronous code: we can put a <code>try<\/code>\/<code>catch<\/code> around an <code>await<\/code>, use local variables on either side of it, return a value and generally reason about the method in source order. When execution reaches an <code>await<\/code> for something that isn&#8217;t yet complete, however, the method can&#8217;t simply leave its current stack frame in place and wait for the operation to finish. The thread needs to be freed up to do other work, while the work after the <code>await<\/code>, including whatever local state it will need later, must survive somewhere. In C#, the compiler has traditionally been responsible for transforming the method into a representation that enables that continuation.<\/p>\n<p>I went into the history and mechanics of that transformation in <a href=\"https:\/\/devblogs.microsoft.com\/dotnet\/how-async-await-really-works\/\">How async\/await really works<\/a>. The very short version is that the compiler traditionally replaces an <code>async<\/code> method with a small entry method and a generated state machine whose <code>MoveNext<\/code> method contains the transformed user code. Parameters, locals that need to survive an incomplete await, spilled expression values, awaiters, the current state number, and a method builder all become fields on a heap-allocated object. The generated <code>MoveNext<\/code> method runs the user&#8217;s code until an awaiter reports that it isn&#8217;t yet complete. It stores enough information to know where and with what values to resume, registers <code>MoveNext<\/code> as the continuation, and returns. When the operation completes, <code>MoveNext<\/code> is invoked again, jumps to the right location based on the saved state number (think <code>goto<\/code> and a label), retrieves the result from a value-producing awaiter, and continues. If every awaiter is already complete, <code>MoveNext<\/code> can run all the way through synchronously. When the method completes or throws, the builder publishes the result, cancellation, or exception through the returned <code>Task<\/code>, <code>Task&lt;T&gt;<\/code>, <code>ValueTask<\/code>, or <code>ValueTask&lt;T&gt;<\/code> (or, in the rare case, a custom task-like type).<\/p>\n<p>For example, consider this tiny method:<\/p>\n<pre><code class=\"language-csharp\">static async Task&lt;int&gt; ReadLengthAsync(Stream stream, CancellationToken cancellationToken)\r\n{\r\n    var buffer = new byte[4096];\r\n    int bytesRead = await stream.ReadAsync(buffer, 0, buffer.Length, cancellationToken);\r\n    return bytesRead;\r\n}<\/code><\/pre>\n<p>While the code that gets generated for this changes over time and differs between debug and release builds, the lowering by the C# compiler has looked something like this:<\/p>\n<pre><code class=\"language-csharp\">[AsyncStateMachine(typeof(&lt;ReadLengthAsync&gt;d__0))]\r\nstatic Task&lt;int&gt; ReadLengthAsync(Stream stream, CancellationToken cancellationToken)\r\n{\r\n    &lt;ReadLengthAsync&gt;d__0 stateMachine = default;\r\n    stateMachine.builder = AsyncTaskMethodBuilder&lt;int&gt;.Create();\r\n    stateMachine.state = -1;\r\n    stateMachine.stream = stream;\r\n    stateMachine.cancellationToken = cancellationToken;\r\n    stateMachine.builder.Start(ref stateMachine);\r\n    return stateMachine.builder.Task;\r\n}\r\n\r\nstruct &lt;ReadLengthAsync&gt;d__0 : IAsyncStateMachine\r\n{\r\n    public int state;\r\n    public AsyncTaskMethodBuilder&lt;int&gt; builder;\r\n    public Stream stream;\r\n    public CancellationToken cancellationToken;\r\n\r\n    private TaskAwaiter&lt;int&gt; awaiter;\r\n\r\n    public void MoveNext()\r\n    {\r\n        int result;\r\n        try\r\n        {\r\n            TaskAwaiter&lt;int&gt; localAwaiter;\r\n\r\n            if (state != 0)\r\n            {\r\n                byte[] buffer = new byte[4096];\r\n                localAwaiter = stream.ReadAsync(buffer, 0, buffer.Length, cancellationToken).GetAwaiter();\r\n                if (!localAwaiter.IsCompleted)\r\n                {\r\n                    state = 0;\r\n                    awaiter = localAwaiter;\r\n                    builder.AwaitUnsafeOnCompleted(ref localAwaiter, ref this);\r\n                    return;\r\n                }\r\n            }\r\n            else\r\n            {\r\n                localAwaiter = awaiter;\r\n                awaiter = default;\r\n                state = -1;\r\n            }\r\n\r\n            result = localAwaiter.GetResult();\r\n        }\r\n        catch (Exception e)\r\n        {\r\n            state = -2;\r\n            builder.SetException(e);\r\n            return;\r\n        }\r\n\r\n        state = -2;\r\n        builder.SetResult(result);\r\n    }\r\n}<\/code><\/pre>\n<p>That&#8217;s quite a lot of generated code for three lines of C#. The compiler has to make decisions before the program runs about the state-machine layout, which values might need to survive, how many awaiter fields are required, and how all the suspension points fit into one <code>MoveNext<\/code> dispatch. The runtime and JIT have optimized the resulting pattern heavily over the years, including combining the task, state machine, continuation, and <code>ExecutionContext<\/code> into a single allocation, but by the time the JIT sees the IL, the transformation has already happened, leaving it with a very complicated system to try to optimize.<\/p>\n<p>.NET 11 introduces a new way to split that responsibility, a reimplementation of the <code>async<\/code>\/<code>await<\/code> infrastructure referred to as &#8220;runtime async&#8221;. Rather than the C# compiler being responsible for the transformation, the JIT is. The C# compiler emits a much smaller suspension-aware IL contract for each eligible <code>async<\/code> method and marks the method as <code>async<\/code> in metadata. The runtime and JIT then do the work that depends on runtime knowledge: creating the externally visible <code>Task<\/code> or <code>ValueTask<\/code>, recognizing direct async calls, deciding which values are actually alive at each suspension point, laying out continuation objects, and generating the control flow that suspends and resumes the method. Effectively, the transformation moves from C# to the runtime, where more information is available to optimize it.<\/p>\n<p>The programming model hasn&#8217;t changed. This is still C# <code>async<\/code>\/<code>await<\/code>; <code>await<\/code> still obeys the awaiter pattern, exceptions and cancellation still surface through the returned task-like object, <code>ConfigureAwait<\/code> still has its usual meaning, synchronous completion is still synchronous completion, and on and on. An explicit goal for the feature has been 100% behavioral compatibility: whether an <code>async<\/code> method is lowered by the language compiler or by the runtime is an implementation detail, and any observable semantic difference is a bug.<\/p>\n<p>In .NET 11, application code opts in with a compiler feature switch:<\/p>\n<pre><code class=\"language-xml\">&lt;Project Sdk=\"Microsoft.NET.Sdk\"&gt;\r\n  &lt;PropertyGroup&gt;\r\n    &lt;TargetFramework&gt;net11.0&lt;\/TargetFramework&gt;\r\n    &lt;Features&gt;$(Features);runtime-async=on&lt;\/Features&gt;\r\n  &lt;\/PropertyGroup&gt;\r\n&lt;\/Project&gt;<\/code><\/pre>\n<p>Note that there&#8217;s no new C# syntax involved, so <code>LangVersion=preview<\/code> isn&#8217;t required, nor is <code>EnablePreviewFeatures<\/code>. While this is opt-in at the application layer, most of the in-box shared framework is already built this way for .NET 11. The <code>async<\/code>\/<code>await<\/code> performance goal for .NET 11 is parity with .NET 10, and in general runtime async is already as good as or better than the older implementation in many important paths. It isn&#8217;t yet fully optimized, though, and there are known cases where it still produces less efficient code. I&#8217;d encourage you to experiment in .NET 11 with opting-in your applications and services; just make sure to measure. My hope is that it&#8217;ll be on by default starting in .NET 12.<\/p>\n<p>Moving the transformation from the C# compiler to the runtime has the added benefit of reducing binary size. As noted, the traditional lowering emits an entry method, a generated state-machine type, fields for captured state, and a <code>MoveNext<\/code> body, for every async method. Runtime async leaves a much smaller method body for the runtime to transform. The following tiny app contains ten <code>Task&lt;int&gt;<\/code>-returning async methods, each awaiting the next, and compiles the same source once with compiler lowering and once with runtime async:<\/p>\n<pre><code class=\"language-xml\">&lt;Project Sdk=\"Microsoft.NET.Sdk\"&gt;\r\n  &lt;PropertyGroup&gt;\r\n    &lt;OutputType&gt;Exe&lt;\/OutputType&gt;\r\n    &lt;TargetFramework&gt;net11.0&lt;\/TargetFramework&gt;\r\n    &lt;AssemblyName&gt;SizeProbe&lt;\/AssemblyName&gt;\r\n    &lt;ImplicitUsings&gt;enable&lt;\/ImplicitUsings&gt;\r\n    &lt;Nullable&gt;enable&lt;\/Nullable&gt;\r\n    &lt;Features Condition=\"'$(RuntimeAsync)' == 'true'\"&gt;$(Features);runtime-async=on&lt;\/Features&gt;\r\n  &lt;\/PropertyGroup&gt;\r\n&lt;\/Project&gt;<\/code><\/pre>\n<pre><code class=\"language-csharp\">\/\/ dotnet build -c Release -p:RuntimeAsync=false -o classic --no-incremental; dotnet build -c Release -p:RuntimeAsync=true -o runtime --no-incremental; Get-Item .\\classic\\SizeProbe.dll, .\\runtime\\SizeProbe.dll | Select-Object Directory, Length\r\n\r\nConsole.WriteLine(await Benchmarks.Layer0());\r\n\r\npublic class Benchmarks\r\n{\r\n    public static async Task&lt;int&gt; Layer0() =&gt; await Layer1();\r\n    private static async Task&lt;int&gt; Layer1() =&gt; await Layer2();\r\n    private static async Task&lt;int&gt; Layer2() =&gt; await Layer3();\r\n    private static async Task&lt;int&gt; Layer3() =&gt; await Layer4();\r\n    private static async Task&lt;int&gt; Layer4() =&gt; await Layer5();\r\n    private static async Task&lt;int&gt; Layer5() =&gt; await Layer6();\r\n    private static async Task&lt;int&gt; Layer6() =&gt; await Layer7();\r\n    private static async Task&lt;int&gt; Layer7() =&gt; await Layer8();\r\n    private static async Task&lt;int&gt; Layer8() =&gt; await Layer9();\r\n\r\n    private static async Task&lt;int&gt; Layer9()\r\n    {\r\n        await Task.Yield();\r\n        return 42;\r\n    }\r\n}<\/code><\/pre>\n<table>\n<thead>\n<tr>\n<th>Lowering<\/th>\n<th style=\"text-align: right;\">SizeProbe.dll<\/th>\n<th style=\"text-align: right;\">Ratio<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td>Compiler<\/td>\n<td style=\"text-align: right;\">10,752 bytes<\/td>\n<td style=\"text-align: right;\">1.00<\/td>\n<\/tr>\n<tr>\n<td>Runtime async<\/td>\n<td style=\"text-align: right;\">5,632 bytes<\/td>\n<td style=\"text-align: right;\">0.52<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<p>For a method such as:<\/p>\n<pre><code class=\"language-csharp\">static async Task&lt;int&gt; CallerAsync() =&gt; await CalleeAsync();<\/code><\/pre>\n<p>with runtime async enabled, the C# compiler generates IL like the following:<\/p>\n<pre><code class=\"language-asm\">; MSIL\r\n.method private hidebysig static\r\n    class System.Threading.Tasks.Task`1&lt;int32&gt; CallerAsync() cil managed async\r\n{\r\n    call class System.Threading.Tasks.Task`1&lt;int32&gt; CalleeAsync()\r\n    call int32 System.Runtime.CompilerServices.AsyncHelpers::Await&lt;int32&gt;(\r\n        class System.Threading.Tasks.Task`1&lt;int32&gt;)\r\n    ret\r\n}<\/code><\/pre>\n<p>There is no generated <code>&lt;CallerAsync&gt;d__0<\/code> type, no <code>IAsyncStateMachine<\/code>, no <code>MoveNext<\/code>, no <code>AsyncTaskMethodBuilder&lt;int&gt;<\/code>, and no <code>AsyncStateMachineAttribute<\/code>. Previously, <code>async<\/code> on a C# method evaporated at compile time. Now, the method has a new <code>MethodImpl<\/code> <code>async<\/code> bit, represented in IL assembly syntax by that <code>async<\/code> modifier, and the body calls helpers in <code>System.Runtime.CompilerServices.AsyncHelpers<\/code>.<\/p>\n<p>At first glance the <code>ret<\/code> looks impossible because the declared signature returns <code>Task&lt;int&gt;<\/code> while the value on the IL evaluation stack is an <code>int<\/code>. This clearly isn&#8217;t a normal calling convention. The VM can give a <code>Task<\/code>-returning method two related identities, or MethodDescs, where one has the normal signature the rest of managed code sees, <code>Task&lt;int&gt; CallerAsync()<\/code>. The other is the AsyncCall variant, which effectively returns <code>int<\/code> and has an implicit channel for a continuation. Both refer to the same logical method and metadata token, but they have different calling conventions and different jobs. If regular managed code invokes <code>CallerAsync<\/code>, the VM-generated outer thunk preserves the public contract and returns a <code>Task&lt;int&gt;<\/code>. If another runtime async method directly awaits it, the JIT can instead call the AsyncCall variant and receive the result directly when the call completes synchronously, or a continuation when it suspends. In other words, it can hand back the <code>T<\/code> directly and avoid allocating a <code>Task&lt;T&gt;<\/code>.<\/p>\n<p>That pairing works in both directions. For a method compiled with runtime async, the AsyncCall variant owns the generated (newly compact) IL while the public <code>Task<\/code>-returning entry point is an adapter thunk; for a traditionally compiled method, the public method owns its usual IL while the VM can create an AsyncCall adapter around it. That means runtime async code remains able to await existing libraries and code compiled by older compilers, a critical capability for our goal of 100% compat. The largest wins naturally appear as more of an async call chain is compiled with runtime async.<\/p>\n<p>This is where the JIT gets an opportunity that simply didn&#8217;t exist when every boundary was already expressed as a task and a generated state machine. Suppose <code>A<\/code> awaits <code>B<\/code>, which awaits <code>C<\/code>:<\/p>\n<pre><code class=\"language-csharp\">static async Task&lt;int&gt; A(bool yield) =&gt; await B(yield);\r\nstatic async Task&lt;int&gt; B(bool yield) =&gt; await C(yield);\r\nstatic async Task&lt;int&gt; C(bool yield)\r\n{\r\n    if (yield)\r\n        await Task.Yield();\r\n\r\n    return 42;\r\n}<\/code><\/pre>\n<p>Traditionally, each method has its own compiler-generated state machine and its own task-like result. <code>C<\/code> suspends and eventually completes its task, which wakes <code>B<\/code>&#8216;s state machine; <code>B<\/code> then completes its task, which wakes <code>A<\/code>&#8216;s state machine; and <code>A<\/code> completes the root task observed by the caller. There has been an enormous amount of work done over the years to reduce the costs of those objects and transitions.<\/p>\n<p>With runtime async, the importer recognizes the adjacent pattern of &#8220;call a Task-returning method, then await that task.&#8221; In the simple case it can call the callee&#8217;s AsyncCall variant instead. When <code>yield<\/code> is false and <code>C<\/code> completes synchronously, the <code>int<\/code> flows back through <code>B<\/code> and <code>A<\/code> as a plain value, and only the outermost boundary needs to turn it into the <code>Task&lt;int&gt;<\/code> promised to the original caller. When <code>yield<\/code> is true and <code>C<\/code> suspends, the runtime links continuation state for the chain and eventually resumes it without requiring an intermediate <code>Task&lt;int&gt;<\/code> at every directly fused edge. The <code>Task<\/code> contract hasn&#8217;t vanished, it just moved to the place where a <code>Task<\/code> is actually needed.<\/p>\n<p>Runtime async doesn&#8217;t make every asynchronous operation allocation-free, though. Rather, it gives the JIT enough information to avoid materializing some task objects that existed only to carry a result from one async method directly into the next. If a consumer stores the task in a collection, manually hooks up a continuation, or otherwise observes the task as an object, that object is still needed. The optimization is about not paying for boundaries that aren&#8217;t observably boundaries.<\/p>\n<p>The impact is already visible with just two layers:<\/p>\n<pre><code class=\"language-csharp\">\/\/ dotnet run -c Release -f net11.0 --filter \"*\"\r\n\/\/ The project also needs the `runtime-async=on` feature switch set.\r\n\r\nusing BenchmarkDotNet.Attributes;\r\nusing BenchmarkDotNet.Configs;\r\nusing BenchmarkDotNet.Running;\r\nusing System.Runtime.CompilerServices;\r\n\r\nBenchmarkSwitcher.FromAssembly(typeof(Benchmarks).Assembly).Run(args);\r\n\r\n[MemoryDiagnoser(false)]\r\n[GroupBenchmarksBy(BenchmarkLogicalGroupRule.ByCategory)]\r\n[HideColumns(\"Job\", \"Error\", \"StdDev\", \"Median\", \"RatioSD\")]\r\npublic class Benchmarks\r\n{\r\n    private static readonly Task&lt;int&gt; s_completed = Task.FromResult(42);\r\n\r\n    [Benchmark(Baseline = true), BenchmarkCategory(\"Completed\")]\r\n    public Task&lt;int&gt; ClassicCompleted() =&gt; ClassicCompletedOuter();\r\n\r\n    [Benchmark, BenchmarkCategory(\"Completed\")]\r\n    public Task&lt;int&gt; RuntimeCompleted() =&gt; RuntimeCompletedOuter();\r\n\r\n    [Benchmark(Baseline = true), BenchmarkCategory(\"Yielding\")]\r\n    public Task&lt;int&gt; ClassicYielding() =&gt; ClassicYieldingOuter();\r\n\r\n    [Benchmark, BenchmarkCategory(\"Yielding\")]\r\n    public Task&lt;int&gt; RuntimeYielding() =&gt; RuntimeYieldingOuter();\r\n\r\n    [RuntimeAsyncMethodGeneration(false)]\r\n    private static async Task&lt;int&gt; ClassicCompletedOuter() =&gt; await ClassicCompletedInner();\r\n\r\n    [RuntimeAsyncMethodGeneration(false)]\r\n    private static async Task&lt;int&gt; ClassicCompletedInner() =&gt; await s_completed;\r\n\r\n    private static async Task&lt;int&gt; RuntimeCompletedOuter() =&gt; await RuntimeCompletedInner();\r\n\r\n    private static async Task&lt;int&gt; RuntimeCompletedInner() =&gt; await s_completed;\r\n\r\n    [RuntimeAsyncMethodGeneration(false)]\r\n    private static async Task&lt;int&gt; ClassicYieldingOuter() =&gt; await ClassicYieldingInner();\r\n\r\n    [RuntimeAsyncMethodGeneration(false)]\r\n    private static async Task&lt;int&gt; ClassicYieldingInner()\r\n    {\r\n        await Task.Yield();\r\n        return 42;\r\n    }\r\n\r\n    private static async Task&lt;int&gt; RuntimeYieldingOuter() =&gt; await RuntimeYieldingInner();\r\n\r\n    private static async Task&lt;int&gt; RuntimeYieldingInner()\r\n    {\r\n        await Task.Yield();\r\n        return 42;\r\n    }\r\n}\r\n\r\nnamespace System.Runtime.CompilerServices\r\n{\r\n    [AttributeUsage(AttributeTargets.Method)]\r\n    internal sealed class RuntimeAsyncMethodGenerationAttribute(bool runtimeAsync) : Attribute\r\n    {\r\n        public bool RuntimeAsync =&gt; runtimeAsync;\r\n    }\r\n}<\/code><\/pre>\n<table>\n<thead>\n<tr>\n<th>Method<\/th>\n<th style=\"text-align: right;\">Mean<\/th>\n<th style=\"text-align: right;\">Ratio<\/th>\n<th style=\"text-align: right;\">Allocated<\/th>\n<th style=\"text-align: right;\">Alloc Ratio<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td>ClassicCompleted<\/td>\n<td style=\"text-align: right;\">21.221 ns<\/td>\n<td style=\"text-align: right;\">1.00<\/td>\n<td style=\"text-align: right;\">144 B<\/td>\n<td style=\"text-align: right;\">1.00<\/td>\n<\/tr>\n<tr>\n<td>RuntimeCompleted<\/td>\n<td style=\"text-align: right;\">6.151 ns<\/td>\n<td style=\"text-align: right;\">0.29<\/td>\n<td style=\"text-align: right;\">0 B<\/td>\n<td style=\"text-align: right;\">0.00<\/td>\n<\/tr>\n<tr>\n<td>ClassicYielding<\/td>\n<td style=\"text-align: right;\">254.139 ns<\/td>\n<td style=\"text-align: right;\">1.00<\/td>\n<td style=\"text-align: right;\">248 B<\/td>\n<td style=\"text-align: right;\">1.00<\/td>\n<\/tr>\n<tr>\n<td>RuntimeYielding<\/td>\n<td style=\"text-align: right;\">116.927 ns<\/td>\n<td style=\"text-align: right;\">0.46<\/td>\n<td style=\"text-align: right;\">168 B<\/td>\n<td style=\"text-align: right;\">0.68<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<p>The synchronously completing chain is more than 3x faster and avoids both\nintermediate task allocations. Even after a real suspension, the same\ntwo-layer chain takes less than half the time and allocates 80 fewer bytes.<\/p>\n<p>Exception handling amplifies the difference. Again consider an async method <code>A<\/code> calling an async method <code>B<\/code> calling an async method <code>C<\/code>. The transformation generated by the C# compiler of each method results in a <code>try<\/code>\/<code>catch<\/code> block around the whole body of the <code>MoveNext<\/code> method so that any unhandled exception can be stored into the returned <code>Task<\/code>. Let&#8217;s say code in <code>C<\/code> throws an unhandled exception. That&#8217;s then caught by this manufactured <code>catch<\/code> block and stored into the <code>Task<\/code> returned to <code>B<\/code>. The awaiter in <code>B<\/code> then retrieves that exception from the <code>Task<\/code> object and throws it. It&#8217;s then caught by <code>B<\/code>&#8216;s generated catch and stored into its <code>Task<\/code>. And so on. An exception crossing ten such async helpers can therefore be thrown, caught, and stored ten times even though none of the source methods has an explicit handler. That is super expensive. But runtime async doesn&#8217;t need to re-enter a pass-through frame with no handler. On the synchronous path the exception unwinds through the fused calls normally, and after a real suspension, one dispatch-loop catch walks past continuation records that have no handler and faults the observable root task once.<\/p>\n<p>The following benchmark measures both a fully synchronous throw and an exception after one real <code>Task.Yield<\/code> suspension. It uses a compiler-recognized per-method escape hatch (<code>RuntimeAsyncMethodGeneration<\/code>) so that the classic and runtime async methods run in the same process on the same .NET 11 runtime and differ only in how the compiler lowers them. (Note that this attribute is experimental and isn&#8217;t a public API exposed from the core libraries; as with other attributes known to the C# compiler, it recognizes them by name and signature.)<\/p>\n<pre><code class=\"language-csharp\">\/\/ dotnet run -c Release -f net11.0 --filter \"*\"\r\n\/\/ The project also needs the `runtime-async=on` feature switch set.\r\n\r\nusing BenchmarkDotNet.Attributes;\r\nusing BenchmarkDotNet.Running;\r\nusing System.Runtime.CompilerServices;\r\n\r\nBenchmarkSwitcher.FromAssembly(typeof(Benchmarks).Assembly).Run(args);\r\n\r\n[MemoryDiagnoser(false), HideColumns(\"Job\", \"Error\", \"StdDev\", \"Median\", \"RatioSD\")]\r\npublic class Benchmarks\r\n{\r\n    [Params(1, 10, 30)]\r\n    public int Depth;\r\n\r\n    [Params(false, true)]\r\n    public bool Yield;\r\n\r\n    [Benchmark(Baseline = true)]\r\n    public int Classic() =&gt; Invoke(ClassicThrowAsync(Depth));\r\n\r\n    [Benchmark]\r\n    public int Runtime() =&gt; Invoke(RuntimeThrowAsync(Depth));\r\n\r\n    private static int Invoke(Task&lt;int&gt; task)\r\n    {\r\n        try\r\n        {\r\n            return task.GetAwaiter().GetResult();\r\n        }\r\n        catch (InvalidOperationException)\r\n        {\r\n            return -1;\r\n        }\r\n    }\r\n\r\n    [RuntimeAsyncMethodGeneration(false)]\r\n    private async Task&lt;int&gt; ClassicThrowAsync(int depth)\r\n    {\r\n        if (depth == 0)\r\n        {\r\n            if (Yield) await Task.Yield();\r\n            throw new InvalidOperationException(\"uh oh\");\r\n        }\r\n\r\n        return await ClassicThrowAsync(depth - 1);\r\n    }\r\n\r\n    private async Task&lt;int&gt; RuntimeThrowAsync(int depth)\r\n    {\r\n        if (depth == 0)\r\n        {\r\n            if (Yield) await Task.Yield();\r\n            throw new InvalidOperationException(\"uh oh\");\r\n        }\r\n\r\n        return await RuntimeThrowAsync(depth - 1);\r\n    }\r\n}\r\n\r\nnamespace System.Runtime.CompilerServices\r\n{\r\n    [AttributeUsage(AttributeTargets.Method)]\r\n    internal sealed class RuntimeAsyncMethodGenerationAttribute(bool runtimeAsync) : Attribute\r\n    {\r\n        public bool RuntimeAsync =&gt; runtimeAsync;\r\n    }\r\n}<\/code><\/pre>\n<table>\n<thead>\n<tr>\n<th>Depth<\/th>\n<th>Yield<\/th>\n<th>Method<\/th>\n<th>Mean<\/th>\n<th>Ratio<\/th>\n<th>Allocated<\/th>\n<th>Alloc Ratio<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td>1<\/td>\n<td>False<\/td>\n<td>Classic<\/td>\n<td>4.727 \u03bcs<\/td>\n<td>1.00<\/td>\n<td>1.6 KB<\/td>\n<td>1.00<\/td>\n<\/tr>\n<tr>\n<td>1<\/td>\n<td>False<\/td>\n<td>Runtime<\/td>\n<td>3.558 \u03bcs<\/td>\n<td>0.75<\/td>\n<td>1.16 KB<\/td>\n<td>0.72<\/td>\n<\/tr>\n<tr>\n<td>1<\/td>\n<td>True<\/td>\n<td>Classic<\/td>\n<td>6.308 \u03bcs<\/td>\n<td>1.00<\/td>\n<td>1.68 KB<\/td>\n<td>1.00<\/td>\n<\/tr>\n<tr>\n<td>1<\/td>\n<td>True<\/td>\n<td>Runtime<\/td>\n<td>8.211 \u03bcs<\/td>\n<td>1.30<\/td>\n<td>1.42 KB<\/td>\n<td>0.85<\/td>\n<\/tr>\n<tr>\n<td>10<\/td>\n<td>False<\/td>\n<td>Classic<\/td>\n<td>19.469 \u03bcs<\/td>\n<td>1.00<\/td>\n<td>15.13 KB<\/td>\n<td>1.00<\/td>\n<\/tr>\n<tr>\n<td>10<\/td>\n<td>False<\/td>\n<td>Runtime<\/td>\n<td>5.885 \u03bcs<\/td>\n<td>0.30<\/td>\n<td>2.13 KB<\/td>\n<td>0.14<\/td>\n<\/tr>\n<tr>\n<td>10<\/td>\n<td>True<\/td>\n<td>Classic<\/td>\n<td>24.923 \u03bcs<\/td>\n<td>1.00<\/td>\n<td>15.63 KB<\/td>\n<td>1.00<\/td>\n<\/tr>\n<tr>\n<td>10<\/td>\n<td>True<\/td>\n<td>Runtime<\/td>\n<td>6.122 \u03bcs<\/td>\n<td>0.25<\/td>\n<td>2.88 KB<\/td>\n<td>0.18<\/td>\n<\/tr>\n<tr>\n<td>30<\/td>\n<td>False<\/td>\n<td>Classic<\/td>\n<td>51.302 \u03bcs<\/td>\n<td>1.00<\/td>\n<td>84.2 KB<\/td>\n<td>1.00<\/td>\n<\/tr>\n<tr>\n<td>30<\/td>\n<td>False<\/td>\n<td>Runtime<\/td>\n<td>10.721 \u03bcs<\/td>\n<td>0.21<\/td>\n<td>5.71 KB<\/td>\n<td>0.07<\/td>\n<\/tr>\n<tr>\n<td>30<\/td>\n<td>True<\/td>\n<td>Classic<\/td>\n<td>65.974 \u03bcs<\/td>\n<td>1.00<\/td>\n<td>85.53 KB<\/td>\n<td>1.00<\/td>\n<\/tr>\n<tr>\n<td>30<\/td>\n<td>True<\/td>\n<td>Runtime<\/td>\n<td>11.254 \u03bcs<\/td>\n<td>0.17<\/td>\n<td>7.72 KB<\/td>\n<td>0.09<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<p>Runtime async supports <code>Task<\/code>, <code>Task&lt;T&gt;<\/code>, <code>ValueTask<\/code>, and <code>ValueTask&lt;T&gt;<\/code> as method return types, but as of today it doesn&#8217;t support <code>async void<\/code>, async iterators, or arbitrary custom task-like return types with custom builders; those continue to use the traditional compiler transformation. For <code>ValueTask&lt;T&gt;<\/code>, the existing reasons to use the type still apply. A <code>ValueTask&lt;T&gt;<\/code> can carry a result directly, wrap a <code>Task&lt;T&gt;<\/code>, or refer to an <code>IValueTaskSource&lt;T&gt;<\/code>. That&#8217;s made it useful for APIs where synchronous completion is common enough that avoiding a <code>Task<\/code> allocation outweighs the larger return value and the more restrictive consumption rules, or where asynchronous completion can have its costs amortized via a reusable backing object. Runtime async then addresses some of the scenarios that would have led developers to use <code>ValueTask&lt;T&gt;<\/code>. Does that mean everyone should stop using <code>ValueTask&lt;T&gt;<\/code>? No. Choosing <code>Task<\/code> versus <code>ValueTask<\/code> remains an API design decision based on completion patterns, allocation sensitivity, call frequency, and how consumers need to use the result. Write the return type that makes sense for the API, then let the compiler, VM, and JIT optimize it as best they can.<\/p>\n<p>Workloads with many layers of small async methods can benefit the most from runtime\nasync, because those layers are exactly where intermediate tasks and state\nmachines often accumulate. Shared framework code, for example, is full of this\npattern: a public method validates arguments and awaits a private helper, which\nawaits a transport helper, which awaits an operating-system operation.\nApplication services similarly compose authentication, retry, logging,\nserialization, and I\/O helpers. Runtime async can make the source-level\ndecomposition cheaper without asking the developer to flatten the code into\none giant method in order to avoid &#8220;implementation detail&#8221; costs.<\/p>\n<p>The work required to reach this point has been extensive. A GitHub search of the <a href=\"https:\/\/github.com\/dotnet\/runtime\/issues?q=label%3Aruntime-async%20is%3Apr\">runtime async tracking label<\/a> on September 14, 2026 returned 235 pull requests, far too many for me to enumerate one by one. So I won&#8217;t try; you can peruse that label in your spare time. The work is also not only about direct performance improvements but also about\nimprovements to diagnostics and performance tooling that help you to make better\nuse of async in your own code. When an async method\nsuspends, its physical thread stack unwinds. That method&#8217;s continuation might later run\non a different thread whose physical stack begins in the thread pool, with the\nmethods that led to the original <code>await<\/code> nowhere to be found. A sampling\nCPU profiler can see where the processor is spending time, but without additional\ninformation, it can&#8217;t reliably connect those traces back through the logical async\ncall chain, making it hard to answer questions about what async call paths were actually costing.\nProfiling tools like the async profiler in Visual Studio have traditionally reconstructed those chains from\nevents emitted by <code>Task<\/code>&#8216;s infrastructure, but async-heavy applications can generate enormous volumes of\nthose very chatty events. The resulting overhead easily perturbs the workload being measured, making\nit all but unusable in production. <a href=\"https:\/\/github.com\/dotnet\/runtime\/pull\/127238\">dotnet\/runtime#127238<\/a> added a\nnew lightweight async-profiler event stream for .NET 11 and runtime async. Rather than sending every small\ntransition through the eventing system as its own full event, the runtime\nwrites compact records into per-thread buffers, delta-encoding timestamps and\ninstruction pointers and flushing the data in batches. It also puts a small\nidentifiable wrapper frame into the physical stack when invoking a\ncontinuation. A profiler can use that frame as an anchor, joining ordinary CPU\nsamples to the logical async call stack represented by the event stream. In some measurements,\nthis new approach added less than 1% overhead and shrank the traced data by an order of magnitude.\n<a href=\"https:\/\/github.com\/dotnet\/runtime\/pull\/129043\">dotnet\/runtime#129043<\/a> and a few follow-up PRs extended\nthe same approach to the compiler-generated state machines used by existing\nasync code. Thus this\nisn&#8217;t useful only to applications that opt into runtime async; tooling gets one\nconsistent representation across both implementations.<\/p>\n<p>What should you as a developer do differently with runtime async in the picture? Mostly nothing. Keep writing asynchronous code the way you want it to read, and break a large operation into helpers when that makes the code clearer. Use <code>Task<\/code> by default and choose <code>ValueTask<\/code> where its API and usage tradeoffs genuinely fit. And don&#8217;t contort source code to remove a clean <code>await<\/code> just because today&#8217;s implementation might allocate an intermediate <code>Task<\/code>. The lowering strategy should &#8220;just work&#8221; as an implementation detail, preserve behavior, and make existing source get better as the runtime improves.<\/p>\n<h3>Bounds Checks<\/h3>\n<p>C# is a memory-safe language. Accesses to arrays, strings, and spans are guaranteed by the runtime to be in-bounds; if you try to access <code>someArray[i]<\/code>, <code>someString[i]<\/code>, or <code>someSpan[i]<\/code> with an index less than 0 or greater than or equal to the length of the array\/string\/span, you&#8217;ll get an exception, not silently corrupted memory or a process crash. The runtime guarantees that all permitted accesses are within bounds, and that means it needs to be able to prove the access is in bounds. The main method the JIT has for achieving that is by injecting code that performs a bounds check, as if instead of:<\/p>\n<pre><code class=\"language-csharp\">int[] array = ...;\r\nint value = array[i];<\/code><\/pre>\n<p>you&#8217;d written:<\/p>\n<pre><code class=\"language-csharp\">int[] array = ...;\r\nif ((uint)i &gt;= array.Length) throw new IndexOutOfRangeException();\r\nint value = array[i];<\/code><\/pre>\n<p>At the assembly level, a bounds check looks something like:<\/p>\n<pre><code class=\"language-asm\">; x64\r\ncmp ecx, dword ptr [rax+8]        ; compare index with array length\r\njae THROW                         ; unsigned index &gt;= length\r\nmov edx, dword ptr [rax+rcx*4+16] ; load the element<\/code><\/pre>\n<p>The JIT could just inject such code on every access and call it a day, but such code adds overhead, so the JIT works to elide those checks and that overhead wherever it can prove the index is valid. Proving an index is valid means the JIT needs to be able to see from other evidence that it couldn&#8217;t possibly be out of bounds.<\/p>\n<p>The quintessential example of that is a <code>for<\/code> loop over the full contents of an array or span:<\/p>\n<pre><code class=\"language-csharp\">for (int i = 0; i &lt; array.Length; i++)\r\n{\r\n    Use(array[i]);\r\n}<\/code><\/pre>\n<p>The JIT recognizes from this idiom that, within the loop body, <code>i<\/code> is guaranteed to be in the range <code>[0, array.Length)<\/code>, and avoids emitting the bounds check for the <code>array[i]<\/code> access. The JIT has long handled this particular case. Other cases, not so much. Bounds-check elimination has improved in virtually every .NET release; more recent releases added range propagation for derived expressions (<code>.NET 7<\/code> and <code>.NET 8<\/code> saw significant improvements here), SSA-based reasoning (<code>.NET 9<\/code>), and better handling of <code>Span&lt;T&gt;<\/code>, whose length sits in a field rather than an object header, complicating tracking. Each year, the developers contributing to the JIT find new patterns that were being missed, that show up in the wild, and that are fixable. .NET 11 improves several such patterns.<\/p>\n<p>Range analysis in the JIT tracks intervals for each variable, an upper bound and a lower bound. For example, taking the true branch of <code>x &lt; 5<\/code> gives the range for <code>x<\/code> in that branch an upper bound of 4 while taking the true branch of <code>x &gt; 2<\/code> makes the lower bound 3. What about <code>x != 5<\/code>? On the true edge, we know <code>x<\/code> isn&#8217;t 5, and if the current range for <code>x<\/code> is <code>[5, 10]<\/code>, then we know the range must actually be <code>[6, 10]<\/code>&#8230; the lower bound can be tightened because the only value at the lower end is excluded. Similarly, if the range is <code>[0, 5]<\/code>, an <code>x != 5<\/code> assertion tells us the range is actually the narrower <code>[0, 4]<\/code>. Or, at least, that&#8217;s what you&#8217;d hope it would do. The JIT had this relevant comment:<\/p>\n<pre><code class=\"language-cpp\">\/\/ We have a != assertion, but it doesn't tell us much about the interval. So just skip it.\r\ncontinue;<\/code><\/pre>\n<p>In .NET 11, <a href=\"https:\/\/github.com\/dotnet\/runtime\/pull\/121273\">dotnet\/runtime#121273<\/a> replaces that logic with productive reasoning. It checks whether the excluded constant is at either edge of the currently tracked range, adding in the new insights if so. C# list patterns, introduced in C# 11, generate just such comparison sequences. For example, the pattern <code>name is [] or [':'] or [':', not ':', ..]<\/code> lowers to something like this:<\/p>\n<pre><code class=\"language-csharp\">if (name != null)\r\n{\r\n    int num = name.Length;\r\n\r\n    if (num == 0) return true;\r\n\r\n    if (num == 1)\r\n    {\r\n        if (name[0] == ':') return true;\r\n    }\r\n    else if (name[0] == ':' &amp;&amp; name[1] != ':')\r\n    {\r\n        return true;\r\n    }\r\n\r\n    return false;\r\n}<\/code><\/pre>\n<p>Range analysis then proceeds with something like this:<\/p>\n<ol>\n<li>We know that <code>Array.Length<\/code> is never negative, so it has a range of <code>[0, Array.MaxLength]<\/code>.<\/li>\n<li>On the false edge of <code>num == 0<\/code>, we know that <code>num != 0<\/code>, so the range is narrowed now to <code>[1, Array.MaxLength]<\/code>.<\/li>\n<li>Similarly, on the false edge of <code>num == 1<\/code>, we know that <code>num != 1<\/code>, so the range is narrowed now to <code>[2, Array.MaxLength]<\/code>.<\/li>\n<li>We then access <code>name[0]<\/code> and <code>name[1]<\/code>, both of which are guaranteed in bounds based on the lower bound of 2 that was established.<\/li>\n<\/ol>\n<p>Without the <code>!= constant<\/code> tightening, that narrowing wouldn&#8217;t happen, and the bounds checks in step 4 couldn&#8217;t be elided. Thankfully, they now can be in .NET 11. Consider this example:<\/p>\n<pre><code class=\"language-csharp\">\/\/ dotnet run -c Release -f net10.0 --filter \"*\" --runtimes net10.0 net11.0\r\n\r\nusing BenchmarkDotNet.Attributes;\r\nusing BenchmarkDotNet.Running;\r\nusing System.Runtime.CompilerServices;\r\n\r\nBenchmarkSwitcher.FromAssembly(typeof(Benchmarks).Assembly).Run(args);\r\n\r\n[DisassemblyDiagnoser, HideColumns(\"Job\", \"Error\", \"StdDev\", \"Median\", \"RatioSD\")]\r\npublic class Benchmarks\r\n{\r\n    private string[] _inputs = [\"\", \":\", \":x\", \"abc\", \":ab\", \"x\", \"ab:cd\"];\r\n\r\n    [Benchmark]\r\n    public int ClassifyAll()\r\n    {\r\n        int total = 0;\r\n        foreach (string s in _inputs) total += Classify(s);\r\n        return total;\r\n    }\r\n\r\n    [MethodImpl(MethodImplOptions.NoInlining)]\r\n    private static int Classify(ReadOnlySpan&lt;char&gt; name) =&gt;\r\n        name switch\r\n        {\r\n            [] =&gt; 0,\r\n            [':'] =&gt; 1,\r\n            [':', not ':', ..] =&gt; 10 + name[0] + name[1],\r\n            _ =&gt; 3\r\n        };\r\n}<\/code><\/pre>\n<p>In .NET 10, we can see the call to <code>CORINFO_HELP_RNGCHKFAIL<\/code> at the bottom of the method. That&#8217;s the tell-tale sign there was at least one bounds check in the method. With .NET 11, that sign is removed.<\/p>\n<pre><code class=\"language-diff\">; Arm64\r\n--- .NET 10\r\n+++ .NET 11\r\n@@ -10,17 +10,15 @@\r\n             beq     G_M000_IG08\r\n\r\n G_M000_IG04:\r\n-            ldrh    w2, [x0]\r\n-            cmp     w2, #58\r\n+            ldrh    w1, [x0]\r\n+            cmp     w1, #58\r\n             bne     G_M000_IG06\r\n\r\n G_M000_IG05:\r\n-            cmp     w1, #1\r\n-            bls     G_M000_IG11\r\n             ldrh    w0, [x0, #0x02]\r\n             cmp     w0, #58\r\n             beq     G_M000_IG06\r\n-            add     w0, w2, w0\r\n+            add     w0, w1, w0\r\n             add     w0, w0, #10\r\n             b       G_M000_IG07\r\n\r\n@@ -44,8 +42,4 @@\r\n             mov     w0, wzr\r\n             b       G_M000_IG07\r\n\r\n-G_M000_IG11:\r\n-            bl      CORINFO_HELP_RNGCHKFAIL\r\n-            brk     #0\r\n-\r\n-; Total bytes of code 112\r\n+; Total bytes of code 96<\/code><\/pre>\n<p>&#8220;Assertion&#8221; machinery in the JIT propagates learned facts (like the aforementioned range information) between &#8220;basic blocks&#8221; (a sequence of instructions with one entry point, one exit point, and no branches into or out of the middle of it), so information established in block A flows to block B if A &#8220;dominates&#8221; B (meaning the only way to get to B is through A). But what about facts established earlier within the same block? That&#8217;s the gap that <a href=\"https:\/\/github.com\/dotnet\/runtime\/pull\/121527\">dotnet\/runtime#121527<\/a> addresses. Consider this code:<\/p>\n<pre><code class=\"language-csharp\">\/\/ dotnet run -c Release -f net10.0 --filter \"*\" --runtimes net10.0 net11.0\r\n\r\nusing BenchmarkDotNet.Attributes;\r\nusing BenchmarkDotNet.Running;\r\nusing System.Runtime.CompilerServices;\r\n\r\nBenchmarkSwitcher.FromAssembly(typeof(Benchmarks).Assembly).Run(args);\r\n\r\n[DisassemblyDiagnoser, HideColumns(\"Job\", \"Error\", \"StdDev\", \"Median\", \"RatioSD\")]\r\npublic class Benchmarks\r\n{\r\n    private int[] _arr = new int[512];\r\n\r\n    [Benchmark]\r\n    public int RunMany()\r\n    {\r\n        int touched = 0;\r\n        for (int i = 0; i &lt; _arr.Length - 2; i++)\r\n        {\r\n            Test(_arr, i);\r\n            touched++;\r\n        }\r\n        return touched;\r\n    }\r\n\r\n    [MethodImpl(MethodImplOptions.NoInlining)]\r\n    private static void Test(int[] arr, int i)\r\n    {\r\n        arr[i] = 0;  \/\/ 1: establishes 'i &gt;= 0 &amp;&amp; i &lt; arr.Length'\r\n        i++;         \/\/ 2: same block\r\n        if (i &lt; arr.Length) arr[i] = 0;  \/\/ 3: proven safe from 1's assertion\r\n    }\r\n}<\/code><\/pre>\n<p>Statements 1, 2, and 3 are all in the same basic block, up to the conditional; after statement 1 executes, if we reach statement 2, the bounds check on statement 1 passed, we know <code>i &gt;= 0<\/code> and <code>i &lt; arr.Length<\/code>, and after statement 2, <code>i<\/code> becomes <code>i + 1<\/code>. After the <code>if<\/code> guard <code>i &lt; arr.Length<\/code> we know the incremented <code>i<\/code> is still within bounds. But when the range check pass in the .NET 10 JIT examined statement 3&#8217;s bounds check, it saw the assertions propagated from predecessor blocks. Since the assertion from statement 1 is generated within the current block, the range check couldn&#8217;t see it. The PR fixed it to walk the current block&#8217;s tree in execution order, accumulating assertions as it went. When we reach statement 3&#8217;s bounds check, we&#8217;ve already walked past statement 1 and picked up its <code>i &gt;= 0 &amp;&amp; i &lt; arr.Length<\/code> assertion.<\/p>\n<pre><code class=\"language-diff\">; Arm64\r\n--- .NET 10\r\n+++ .NET 11\r\n@@ -13,8 +13,6 @@\r\n             ble     G_M000_IG04\r\n\r\n G_M000_IG03:\r\n-            cmp     w1, w2\r\n-            bhs     G_M000_IG05\r\n             str     wzr, [x0, w1, UXTW #2]\r\n\r\n G_M000_IG04:\r\n@@ -25,4 +23,4 @@\r\n             bl      CORINFO_HELP_RNGCHKFAIL\r\n             brk     #0\r\n\r\n-; Total bytes of code 68\r\n+; Total bytes of code 60<\/code><\/pre>\n<p>There are almost an infinite number of things the JIT could look for and special-case. But every special case requires code, maintenance, and, most importantly, compilation time. A &#8220;just-in-time&#8221; compiler typically runs while the application is running, so the JIT itself must be optimized and spend its limited budget only where there&#8217;s a likely payoff. That pushes the developers building it toward patterns that occur in real workloads. One such pattern, often seen in libraries like format decoders, builds a table\nindex with bitwise operations on a byte, for example\n<code>((b &amp; 0x03) &lt;&lt; 4) | ((b &amp; 0xf0) &gt;&gt; 4)<\/code>. Each masked piece has a tiny upper\nbound, so the OR of those pieces is always in <code>[0..63]<\/code>, safely in range for\ne.g. a Base64 alphabet table. Until\n<a href=\"https:\/\/github.com\/dotnet\/runtime\/pull\/122263\">dotnet\/runtime#122263<\/a>, the JIT\noften failed to prove that combined bound and left a bounds check on the\nindex. Existing range-check code understood the upper bounds produced by\nbitwise AND and shifts, but not OR; the change lets the JIT combine the known\nbounds of both OR operands and remove the remaining array check.<\/p>\n<pre><code class=\"language-csharp\">\/\/ dotnet run -c Release -f net10.0 --filter \"*\" --runtimes net10.0 net11.0\r\n\r\nusing BenchmarkDotNet.Attributes;\r\nusing BenchmarkDotNet.Running;\r\nusing System.Runtime.CompilerServices;\r\n\r\nBenchmarkSwitcher.FromAssembly(typeof(Benchmarks).Assembly).Run(args);\r\n\r\n[DisassemblyDiagnoser, HideColumns(\"Job\", \"Error\", \"StdDev\", \"Median\", \"RatioSD\")]\r\npublic class Benchmarks\r\n{\r\n    private readonly byte[] _input = new byte[4096];\r\n\r\n    [GlobalSetup]\r\n    public void Setup() =&gt; new Random(42).NextBytes(_input);\r\n\r\n    [Benchmark]\r\n    public int Base64LikeIndex() =&gt; Sum(_input);\r\n\r\n    [MethodImpl(MethodImplOptions.NoInlining)]\r\n    private static int Sum(ReadOnlySpan&lt;byte&gt; input)\r\n    {\r\n        int sum = 0;\r\n        foreach (byte b in input)\r\n        {\r\n            int index = ((b &amp; 0x03) &lt;&lt; 4) | ((b &amp; 0xF0) &gt;&gt; 4);\r\n            sum += \"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+\/=\"u8[index];\r\n        }\r\n\r\n        return sum;\r\n    }\r\n}<\/code><\/pre>\n<p>The .NET 10 assembly checks the computed index against the 65-byte lookup\ntable on every iteration. In .NET 11, range analysis proves the index is at\nmost 63, so both the comparison and the branch to the range-check failure\nhelper disappear:<\/p>\n<pre><code class=\"language-diff\">; x64\r\n M01_L00:\r\n        movzx    r9d, byte ptr [rdx+r8]\r\n        mov      r11d, r9d\r\n        and      r11d, 3\r\n        shl      r11d, 4\r\n        and      r9d, 0F0\r\n        sar      r9d, 4\r\n        or       r9d, r11d\r\n-       cmp      r9d, 41\r\n-       jae      short M01_L02\r\n        movzx    r9d, byte ptr [r10+r9]\r\n        add      eax, r9d\r\n        inc      r8d\r\n        cmp      r8d, ecx\r\n        jl       short M01_L00\r\n\r\n-M01_L02:\r\n-       call     CORINFO_HELP_RNGCHKFAIL\r\n-       int      3\r\n-\r\n-; Total bytes of code 95\r\n+; Total bytes of code 79<\/code><\/pre>\n<p>As another example, <a href=\"https:\/\/github.com\/dotnet\/runtime\/pull\/125056\">dotnet\/runtime#125056<\/a> improves the handling of guards like <code>(uint)i &lt; span.Length<\/code> that are pervasive in performance-sensitive code. Consider this benchmark:<\/p>\n<pre><code class=\"language-csharp\">\/\/ dotnet run -c Release -f net10.0 --filter \"*\" --runtimes net10.0 net11.0\r\n\r\nusing BenchmarkDotNet.Attributes;\r\nusing BenchmarkDotNet.Running;\r\nusing System.Runtime.CompilerServices;\r\n\r\nBenchmarkSwitcher.FromAssembly(typeof(Benchmarks).Assembly).Run(args);\r\n\r\n[DisassemblyDiagnoser, HideColumns(\"Job\", \"Error\", \"StdDev\", \"Median\", \"RatioSD\")]\r\npublic class Benchmarks\r\n{\r\n    private int[] _data = Enumerable.Range(0, 512).ToArray();\r\n\r\n    [Benchmark]\r\n    public int RunMany()\r\n    {\r\n        int sum = 0;\r\n        for (int i = 0; i &lt; _data.Length; i++)\r\n            sum += Test(_data, i);\r\n        return sum;\r\n    }\r\n\r\n    [MethodImpl(MethodImplOptions.NoInlining)]\r\n    private static int Test(Span&lt;int&gt; span, int i)\r\n    {\r\n        if ((uint)i &lt; (uint)span.Length)\r\n        {\r\n            if (i != 0)\r\n                return span[i - 1] + span[i];\r\n\r\n            return span[i];\r\n        }\r\n\r\n        return 0;\r\n    }\r\n}<\/code><\/pre>\n<p>Because the comparison is unsigned, <code>(uint)i<\/code> would be a large positive number if <code>i<\/code> were negative, making it impossible for <code>(uint)i &lt; (uint)span.Length<\/code> to be true (since a span&#8217;s length is never negative, <code>(uint)span.Length<\/code> is at most <code>int.MaxValue<\/code>). Inside the true branch, <code>i<\/code> is therefore in <code>[0, span.Length - 1]<\/code>. Previously, the JIT wasn&#8217;t always recording the lower bound <code>i &gt;= 0<\/code> when it processed the <code>(uint)i &lt; span.Length<\/code> assertion, and that could leave bounds checks on expressions like <code>i - 1<\/code> in place. The fix adds the <code>[0, int.MaxValue - 1]<\/code> lower bound deduction for the index variable upon entering the true arm of a <code>(uint)i &lt; span.Length<\/code> check. Combined with the existing range tracking for the upper bound, this gives the JIT a complete picture of <code>i<\/code>&#8216;s range inside the guarded block.<\/p>\n<pre><code class=\"language-diff\">; Arm64\r\n--- .NET 10\r\n+++ .NET 11\r\n@@ -8,10 +8,8 @@\r\n             cbz     w2, G_M000_IG05\r\n\r\n G_M000_IG03:\r\n-            sub     w3, w2, #1\r\n-            cmp     w3, w1\r\n-            bhs     G_M000_IG09\r\n-            ldr     w1, [x0, w3, UXTW #2]\r\n+            sub     w1, w2, #1\r\n+            ldr     w1, [x0, w1, UXTW #2]\r\n             ldr     w0, [x0, w2, UXTW #2]\r\n             add     w0, w1, w0\r\n\r\n@@ -33,8 +31,4 @@\r\n             ldp     fp, lr, [sp], #0x10\r\n             ret     lr\r\n\r\n-G_M000_IG09:\r\n-            bl      CORINFO_HELP_RNGCHKFAIL\r\n-            brk     #0\r\n-\r\n-; Total bytes of code 84\r\n+; Total bytes of code 68<\/code><\/pre>\n<p>Bounds check elision is generally based on forms of range analysis, where the JIT needs to prove that a given index is guaranteed to be within the range of the data structure. But the same range analysis-based facts can prove that other checks are unnecessary. For example, once the JIT knows that an integer is in <code>[0..100]<\/code>, it can prove both that converting it to <code>byte<\/code> can&#8217;t lose data and that multiplying it by 10 can&#8217;t overflow. <a href=\"https:\/\/github.com\/dotnet\/runtime\/pull\/124147\">dotnet\/runtime#124147<\/a> enables the JIT to use such facts to avoid unnecessary branches as part of <code>checked<\/code> operations. When range analysis proves that the operands are in ranges whose result can&#8217;t overflow, making <code>checked<\/code> a nop, the backend can now emit plain add\/multiply\/subtract instructions, without the jump to failure, as in the following example:<\/p>\n<pre><code class=\"language-csharp\">\/\/ dotnet run -c Release -f net10.0 --filter \"*\" --runtimes net10.0 net11.0\r\n\r\nusing BenchmarkDotNet.Attributes;\r\nusing BenchmarkDotNet.Running;\r\nusing System.Runtime.CompilerServices;\r\n\r\nBenchmarkSwitcher.FromAssembly(typeof(Benchmarks).Assembly).Run(args);\r\n\r\n[DisassemblyDiagnoser, HideColumns(\"Job\", \"Error\", \"StdDev\", \"Median\", \"RatioSD\")]\r\npublic class Benchmarks\r\n{\r\n    private readonly int[] _array = new int[99];\r\n\r\n    [Benchmark]\r\n    public int ArrayLengthPlusConstant() =&gt; AddToLength(_array);\r\n\r\n    [MethodImpl(MethodImplOptions.NoInlining)]\r\n    private static int AddToLength(int[] array) =&gt; checked(array.Length + 10);\r\n\r\n    [Benchmark]\r\n    public int GuardedLengthTimesConstant() =&gt; Multiply(_array);\r\n\r\n    [MethodImpl(MethodImplOptions.NoInlining)]\r\n    private static int Multiply(Span&lt;int&gt; span)\r\n    {\r\n        if (span.Length &gt;= 100) return 0;\r\n        return checked(span.Length * 10);\r\n    }\r\n}<\/code><\/pre>\n<pre><code class=\"language-diff\">; Arm64\r\n--- .NET 10\r\n+++ .NET 11\r\n G_M000_IG02:\r\n             cmp     w1, #100\r\n             bge     G_M000_IG05\r\n\r\n G_M000_IG03:\r\n             mov     w0, #10\r\n-            smull   x0, w1, w0\r\n-            lsr     x2, x0, #32\r\n-            cmp     w2, w0, ASR #31\r\n+            mul     w0, w1, w0\r\n-            bne     G_M000_IG07\r\n\r\n G_M000_IG04:\r\n             ldp     fp, lr, [sp], #0x10\r\n             ret     lr\r\n\r\n-G_M000_IG07:\r\n-            bl      CORINFO_HELP_OVERFLOW\r\n-            brk     #0\r\n-\r\n-; Total bytes of code 64\r\n+; Total bytes of code 44<\/code><\/pre>\n<p>That makes the change broadly applicable: any time you write <code>checked<\/code> arithmetic on quantities that are inherently bounded, such as collection counts, lengths, or indices constrained by prior comparisons, the JIT now has a chance to prove at compile time that the overflow can&#8217;t happen and thus eliminate the run-time check entirely. Building on that range-check work, <a href=\"https:\/\/github.com\/dotnet\/runtime\/pull\/124184\">dotnet\/runtime#124184<\/a> teaches the JIT to eliminate &#8220;narrowing casts&#8221; under the same kinds of guards:<\/p>\n<pre><code class=\"language-csharp\">\/\/ dotnet run -c Release -f net10.0 --filter \"*\" --runtimes net10.0 net11.0\r\n\r\nusing BenchmarkDotNet.Attributes;\r\nusing BenchmarkDotNet.Running;\r\nusing System.Runtime.CompilerServices;\r\n\r\nBenchmarkSwitcher.FromAssembly(typeof(Benchmarks).Assembly).Run(args);\r\n\r\n[DisassemblyDiagnoser, HideColumns(\"Job\", \"Error\", \"StdDev\", \"Median\", \"RatioSD\")]\r\npublic class Benchmarks\r\n{\r\n    private uint _value = 100;\r\n\r\n    [Benchmark]\r\n    public byte GuardedNarrowingCast() =&gt; Narrow(_value);\r\n\r\n    [MethodImpl(MethodImplOptions.NoInlining)]\r\n    private static byte Narrow(uint value)\r\n    {\r\n        if (value &gt; 100) return 0;\r\n        return checked((byte)value);\r\n    }\r\n}<\/code><\/pre>\n<pre><code class=\"language-diff\">; Arm64\r\n--- .NET 10\r\n+++ .NET 11\r\n@@ -4,23 +4,10 @@\r\n\r\n G_M000_IG02:\r\n             cmp     w0, #100\r\n-            bhi     G_M000_IG04\r\n-            cmp     w0, #255\r\n-            bhi     G_M000_IG06\r\n+            csel    w0, w0, wzr, ls\r\n\r\n G_M000_IG03:\r\n             ldp     fp, lr, [sp], #0x10\r\n             ret     lr\r\n\r\n-G_M000_IG04:\r\n-            mov     w0, wzr\r\n-\r\n-G_M000_IG05:\r\n-            ldp     fp, lr, [sp], #0x10\r\n-            ret     lr\r\n-\r\n-G_M000_IG06:\r\n-            bl      CORINFO_HELP_OVERFLOW\r\n-            brk     #0\r\n-\r\n-; Total bytes of code 52\r\n+; Total bytes of code 24<\/code><\/pre>\n<p>Such use of <code>checked<\/code> is common in serialization and protocol code where you validate a value&#8217;s range prior to truncating it. In this benchmark I&#8217;ve used <code>checked<\/code> explicitly, but the more common form is with the whole project compiled with <code>&lt;CheckForOverflowUnderflow&gt;true&lt;\/CheckForOverflowUnderflow&gt;<\/code> in the .csproj, such that this <code>checked<\/code> becomes implicit. After the change, the range analysis sees that <code>value<\/code> is in the range <code>[0, 100]<\/code>, knows <code>byte<\/code> fits values up to 255, and elides the check.<\/p>\n<p><a href=\"https:\/\/github.com\/dotnet\/runtime\/pull\/128620\">dotnet\/runtime#128620<\/a> further teaches range analysis the possible results of leading-zero count, trailing-zero count, and population count instructions. Those results are often used to index small lookup tables&#8230; knowing their bounds lets the JIT remove the bounds check.<\/p>\n<pre><code class=\"language-csharp\">\/\/ dotnet run -c Release -f net10.0 --filter \"*\" --runtimes net10.0 net11.0\r\n\r\nusing BenchmarkDotNet.Attributes;\r\nusing BenchmarkDotNet.Running;\r\nusing System.Numerics;\r\n\r\nBenchmarkSwitcher.FromAssembly(typeof(Benchmarks).Assembly).Run(args);\r\n\r\n[HideColumns(\"Job\", \"Error\", \"StdDev\", \"Median\", \"RatioSD\")]\r\npublic class Benchmarks\r\n{\r\n    private static readonly int[] s_lookup =\r\n        Enumerable.Range(0, 33).Select(i =&gt; i * i).ToArray();\r\n    private uint[] _values;\r\n\r\n    [GlobalSetup]\r\n    public void Setup()\r\n    {\r\n        Random rng = new(42);\r\n        _values = Enumerable.Range(0, 1024).Select(i =&gt; (uint)rng.Next(1, int.MaxValue)).ToArray();\r\n    }\r\n\r\n    [Benchmark]\r\n    public int SumLookupByLeadingZeroCount()\r\n    {\r\n        int sum = 0;\r\n        foreach (var v in _values)\r\n            sum += s_lookup[BitOperations.LeadingZeroCount(v)];\r\n\r\n        return sum;\r\n    }\r\n}<\/code><\/pre>\n<p>The lookup improves because the JIT now knows <code>LeadingZeroCount(uint)<\/code> is between 0 and 32 and can remove the bounds check.<\/p>\n<table>\n<thead>\n<tr>\n<th>Method<\/th>\n<th>Runtime<\/th>\n<th>Mean<\/th>\n<th>Ratio<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td>SumLookupByLeadingZeroCount<\/td>\n<td>.NET 10.0<\/td>\n<td>516.1 ns<\/td>\n<td>1.00<\/td>\n<\/tr>\n<tr>\n<td>SumLookupByLeadingZeroCount<\/td>\n<td>.NET 11.0<\/td>\n<td>438.5 ns<\/td>\n<td>0.85<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<p>The JIT is also able to conditionally apply range check-based elision via &#8220;cloning&#8221;. Cloning is a mechanism where the JIT takes one piece of code and duplicates it. One of the copies it leaves as it was originally, and the other copy it special cases. So, for example, if you had code like:<\/p>\n<pre><code class=\"language-csharp\">int value = array[i];<\/code><\/pre>\n<p>the JIT could theoretically clone that in order to avoid the implicit bounds check, e.g.<\/p>\n<pre><code class=\"language-csharp\">int value;\r\nif ((uint)i &lt; array.Length)\r\n{\r\n    \/\/ no bounds check emitted by JIT, e.g.\r\n    value = Unsafe.Add(ref MemoryMarshal.GetArrayDataReference(array), i);\r\n}\r\nelse\r\n{\r\n    \/\/ bounds check emitted\r\n    value = array[i];\r\n}<\/code><\/pre>\n<p>That particular code looks silly, as we&#8217;re just trading an implicit bounds check for an explicit one. It becomes less silly when the JIT is able to elide multiple bounds checks with a single branch, e.g.<\/p>\n<pre><code class=\"language-csharp\">int sum;\r\nif (4 &lt; array.Length)\r\n{\r\n    \/\/ zero bounds checks\r\n    ref int startRef = ref MemoryMarshal.GetArrayDataReference(array);\r\n    sum =\r\n        startRef +\r\n        Unsafe.Add(ref startRef, 1) +\r\n        Unsafe.Add(ref startRef, 2) +\r\n        Unsafe.Add(ref startRef, 3);\r\n}\r\nelse\r\n{\r\n    \/\/ potentially four bounds checks\r\n    sum =\r\n        array[0] +\r\n        array[1] +\r\n        array[2] +\r\n        array[3];\r\n}<\/code><\/pre>\n<p>Such optimizations are already handled in the JIT, via its <code>optRangeCheckCloning<\/code> phase. It groups bounds checks from a basic block, emits one guard for the largest required range, and duplicates the affected code into a fast path where the individual checks can be removed and a fallback path where they remain. However, one long-standing limitation of range-check cloning is that it refused to process the last statement of any &#8220;terminator&#8221; block, a block that ends with a jump or return instruction. For a method like:<\/p>\n<pre><code class=\"language-csharp\">static int ArrayAccess(int[] abcd) =&gt; abcd[0] + abcd[1] + abcd[2] + abcd[3];<\/code><\/pre>\n<p>all four array accesses live in the return statement, the last statement of a return block, so nothing got cloned and the hot path retained four separate bounds checks. In .NET 11, <a href=\"https:\/\/github.com\/dotnet\/runtime\/pull\/124705\">dotnet\/runtime#124705<\/a> removes that restriction, making the return statement eligible for range-check cloning and allowing a single fast-path guard to cover all four accesses.<\/p>\n<p>But even without range-check cloning, there&#8217;s really no reason such accesses should require four bounds checks: the JIT should be able to see that the array or span needs to have a length of at least 4 and guard all accesses by that single check. If there were intervening operations that had side effects, the JIT would need to maintain order of operations, at least enough to maintain the observable behavior of those effects, but that&#8217;s not the case here. With <a href=\"https:\/\/github.com\/dotnet\/runtime\/pull\/127439\">dotnet\/runtime#127439<\/a> in .NET 11, the JIT will now coalesce those checks within a basic block, strengthening the first check to the largest constant index and removing the rest.<\/p>\n<pre><code class=\"language-csharp\">\/\/ dotnet run -c Release -f net10.0 --filter \"*\" --runtimes net10.0 net11.0\r\n\r\nusing BenchmarkDotNet.Attributes;\r\nusing BenchmarkDotNet.Running;\r\n\r\nBenchmarkSwitcher.FromAssembly(typeof(Benchmarks).Assembly).Run(args);\r\n\r\n[HideColumns(\"Job\", \"Error\", \"StdDev\", \"Median\", \"RatioSD\")]\r\npublic class Benchmarks\r\n{\r\n    private readonly int[] _values = Enumerable.Range(0, 16).ToArray();\r\n\r\n    [Benchmark]\r\n    public int Sum16()\r\n    {\r\n        int[] values = _values;\r\n        return\r\n            values[0] + values[1] + values[2] + values[3] +\r\n            values[4] + values[5] + values[6] + values[7] +\r\n            values[8] + values[9] + values[10] + values[11] +\r\n            values[12] + values[13] + values[14] + values[15];\r\n    }\r\n}<\/code><\/pre>\n<p>In previous releases, you&#8217;d sometimes see a proactive developer doing a similar optimization manually, e.g. reordering the accesses in an example like that to put the largest read first. That&#8217;s no longer necessary.<\/p>\n<table>\n<thead>\n<tr>\n<th>Method<\/th>\n<th>Runtime<\/th>\n<th>Mean<\/th>\n<th>Ratio<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td>Sum16<\/td>\n<td>.NET 10.0<\/td>\n<td>2.958 ns<\/td>\n<td>1.00<\/td>\n<\/tr>\n<tr>\n<td>Sum16<\/td>\n<td>.NET 11.0<\/td>\n<td>1.828 ns<\/td>\n<td>0.62<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<p>Another bounds checking improvement comes in <a href=\"https:\/\/github.com\/dotnet\/runtime\/pull\/127488\">dotnet\/runtime#127488<\/a>, which actually targets explicitly-implemented bounds checks (rather than the implicit ones we&#8217;ve been discussing) and targets code that reads a fixed-size value from the end of a span, such as <code>BinaryPrimitives.ReadInt32BigEndian(span.Slice(span.Length - 4))<\/code> behind a <code>span.Length &gt;= 4<\/code> guard, e.g.<\/p>\n<pre><code class=\"language-csharp\">if (span.Length &gt;= 4)\r\n{\r\n    \/\/ Parse an int from the end of the span\r\n    ... = ReadInt32BigEndian(span.Slice(span.Length - 4));\r\n    ...\r\n}<\/code><\/pre>\n<p>There shouldn&#8217;t be any additional bounds checking required here. However, <code>Span.Slice<\/code> begins with:<\/p>\n<pre><code class=\"language-csharp\">if ((uint)start &gt; (uint)_length)\r\n    ThrowHelper.ThrowArgumentOutOfRangeException();<\/code><\/pre>\n<p>and <code>ReadInt32BigEndian<\/code> begins with:<\/p>\n<pre><code class=\"language-csharp\">if (sizeof(T) &gt; source.Length)\r\n    ThrowHelper.ThrowArgumentOutOfRangeException();<\/code><\/pre>\n<p>so even though our <code>span.Length &gt;= 4<\/code> check should have been sufficient, we&#8217;re still ending up with two additional checks. To address that, the JIT needed two things.<\/p>\n<p>First, it needed to be able to identify that <code>x - (x + a)<\/code> is the same as <code>-a<\/code>. Without this identity, <code>length - (length - 4)<\/code> is just an opaque subtraction of two expressions with no obvious constant result. With the identity, the JIT can recognize the inner expression <code>(length - 4)<\/code> as <code>length + (-4)<\/code>, apply <code>x - (x + a) == -a<\/code> with <code>x == length<\/code> and <code>a == -4<\/code>, and end up with <code>-(-4) == 4<\/code>. Now <code>ReadInt32BigEndian<\/code>&#8216;s check against 4 becomes <code>4 &gt;= 4<\/code>, which the JIT can trivially see is true.<\/p>\n<p>Second, <code>Slice(start)<\/code> must establish that <code>start<\/code> is between zero and the span&#8217;s length. When <code>start<\/code> is <code>length - 4<\/code>, the existing <code>length &gt;= 4<\/code> guard proves the result is non-negative, while subtracting a positive constant means the result can&#8217;t exceed <code>length<\/code>. The improved range analysis connects that guard to the subtraction and removes <code>Slice<\/code>&#8216;s check.<\/p>\n<p>Both fixes together mean the above example now elides both extra bounds\nchecks. That&#8217;s useful in particular for libraries like parsers, network\nprotocol implementations, and cryptographic code, all of which frequently on\nhot paths do things like &#8220;read the last N bytes of a buffer.&#8221;<\/p>\n<pre><code class=\"language-csharp\">\/\/ dotnet run -c Release -f net10.0 --filter \"*\" --runtimes net10.0 net11.0\r\n\r\nusing BenchmarkDotNet.Attributes;\r\nusing BenchmarkDotNet.Running;\r\nusing System.Buffers.Binary;\r\nusing System.Runtime.CompilerServices;\r\n\r\nBenchmarkSwitcher.FromAssembly(typeof(Benchmarks).Assembly).Run(args);\r\n\r\n[DisassemblyDiagnoser]\r\n[HideColumns(\"Job\", \"Error\", \"StdDev\", \"Median\", \"RatioSD\")]\r\npublic class Benchmarks\r\n{\r\n    private readonly byte[] _buffer = new byte[64];\r\n\r\n    [Benchmark]\r\n    public int ReadLastInt32() =&gt; ReadLastInt32(_buffer);\r\n\r\n    [MethodImpl(MethodImplOptions.NoInlining)]\r\n    private static int ReadLastInt32(ReadOnlySpan&lt;byte&gt; span)\r\n    {\r\n        if (span.Length &gt;= sizeof(int))\r\n        {\r\n            return BinaryPrimitives.ReadInt32BigEndian(span.Slice(span.Length - sizeof(int)));\r\n        }\r\n\r\n        return -1;\r\n    }\r\n}<\/code><\/pre>\n<p>In .NET 10, the helper is 73 bytes and includes both additional checks and\ntheir throw paths:<\/p>\n<pre><code class=\"language-x86asm\">; x64\r\ncmp       ecx,4\r\njl        RETURN_MINUS_ONE\r\nlea       edx,[rcx-4]\r\ncmp       edx,ecx\r\nja        THROW_SLICE\r\nmov       r8d,edx\r\nadd       rax,r8\r\nsub       ecx,edx\r\ncmp       ecx,4\r\njl        THROW_READ\r\nmovbe     eax,[rax]<\/code><\/pre>\n<p>In .NET 11, the helper is 28 bytes, and only the original length guard remains:<\/p>\n<pre><code class=\"language-x86asm\">; x64\r\ncmp       ecx,4\r\njl        RETURN_MINUS_ONE\r\nadd       ecx,-4\r\nadd       rax,rcx\r\nmovbe     eax,[rax]<\/code><\/pre>\n<p><a href=\"https:\/\/github.com\/dotnet\/runtime\/pull\/122040\">dotnet\/runtime#122040<\/a> and <a href=\"https:\/\/github.com\/dotnet\/runtime\/pull\/127117\">dotnet\/runtime#127117<\/a> similarly help to remove bounds checks involving <code>span.Slice<\/code>. Vectorized loops often work through a span a chunk at a time, slicing off the elements they&#8217;ve already processed. The JIT hasn&#8217;t always been able to keep track of how those progressively smaller slices relate to the original span, so it could end up checking the same limits again on each iteration. These changes improve that tracking, enabling more of those repeated checks to be removed.<\/p>\n<pre><code class=\"language-csharp\">\/\/ dotnet run -c Release -f net10.0 --filter \"*\" --runtimes net10.0 net11.0\r\n\r\nusing System.Runtime.CompilerServices;\r\nusing System.Runtime.Intrinsics;\r\nusing BenchmarkDotNet.Attributes;\r\nusing BenchmarkDotNet.Running;\r\n\r\nBenchmarkSwitcher.FromAssembly(typeof(Benchmarks).Assembly).Run(args);\r\n\r\n[DisassemblyDiagnoser, HideColumns(\"Job\", \"Error\", \"StdDev\", \"Median\", \"RatioSD\")]\r\npublic class Benchmarks\r\n{\r\n    private readonly int[] _data = Enumerable.Repeat(1, 1_024).ToArray();\r\n\r\n    [Benchmark]\r\n    public Vector256&lt;int&gt; CreateFromSlice() =&gt; CreateFromSlice(_data);\r\n\r\n    [Benchmark]\r\n    public int SumSliced() =&gt; SumSliced(_data);\r\n\r\n    [MethodImpl(MethodImplOptions.NoInlining)]\r\n    private static Vector256&lt;int&gt; CreateFromSlice(Span&lt;int&gt; values)\r\n    {\r\n        if (values.Length &lt; 16)\r\n            return default;\r\n\r\n        return Vector256.Create(values.Slice(8));\r\n    }\r\n\r\n    [MethodImpl(MethodImplOptions.NoInlining)]\r\n    private static int SumSliced(ReadOnlySpan&lt;int&gt; data)\r\n    {\r\n        Vector128&lt;int&gt; sum = default;\r\n        while (data.Length &gt;= Vector128&lt;int&gt;.Count)\r\n        {\r\n            sum += Vector128.Create(data);\r\n            data = data.Slice(Vector128&lt;int&gt;.Count);\r\n        }\r\n\r\n        int result = Vector128.Sum(sum);\r\n        foreach (int value in data)\r\n            result += value;\r\n\r\n        return result;\r\n    }\r\n}<\/code><\/pre>\n<p>In .NET 10, the loop condition proves that at least one vector remains, but\nthe construction of the vector from the current span performs the same check\nagain. .NET 11 retains the length relationship, so\nthe loop body begins directly with the vector addition:<\/p>\n<pre><code class=\"language-diff\">; x64, vector loop\r\n-cmp       esi, 4\r\n-jl        THROW_ARGUMENT_OUT_OF_RANGE\r\n-vpaddd    xmm6, xmm6, [rbx]\r\n-add       rbx, 10\r\n-add       esi, 0FFFFFFFC\r\n-cmp       esi, 4\r\n+vpaddd    xmm0, xmm0, [rax]\r\n+add       rax, 10\r\n+add       ecx, 0FFFFFFFC\r\n+cmp       ecx, 4\r\n jge       LOOP<\/code><\/pre>\n<p>We saw earlier how range-check cloning enables duplicating a sequence of instructions in order to eliminate bounds checks. &#8220;Loop cloning&#8221; extends that to a whole loop. Consider a loop that processes the first <code>count<\/code> elements of an array:<\/p>\n<pre><code class=\"language-csharp\">for (int i = 0; i &lt; count; i++)\r\n    sum += values[i];<\/code><\/pre>\n<p>The test <code>i &lt; count<\/code> doesn&#8217;t by itself prove that <code>i &lt; values.Length<\/code>, so by default the compilation would need a bounds check in the body, which would mean a bounds check for every <code>values[i]<\/code> access. Loop cloning gives the JIT another option. Instead of generating the equivalent of:<\/p>\n<pre><code class=\"language-csharp\">for (int i = 0; i &lt; count; i++)\r\n    sum += values[i]; \/\/ bounds check!<\/code><\/pre>\n<p>it can generate the equivalent of:<\/p>\n<pre><code class=\"language-csharp\">if ((uint)count &lt;= (uint)values.Length)\r\n{\r\n    \/\/ no bounds checks\r\n    ref int startRef = ref MemoryMarshal.GetArrayDataReference(values);\r\n    for (int i = 0; i &lt; count; i++)\r\n    {\r\n        sum += Unsafe.Add(ref startRef, i);\r\n    }\r\n}\r\nelse\r\n{\r\n    \/\/ bounds check per iteration\r\n    for (int i = 0; i &lt; count; i++)\r\n    {\r\n        sum += values[i];\r\n    }\r\n}<\/code><\/pre>\n<p>For the common case where the iteration is in bounds, execution proceeds through a cloned loop with no per-iteration bounds checks, whereas the original checked loop remains as the fallback that preserves exceptional behavior for invalid inputs. The normal path pays for one guard and avoids a check on every iteration, but that comes at the expense of duplicating code. The JIT therefore needs to apply the optimization selectively.<\/p>\n<p>The JIT has long employed loop cloning, but it didn&#8217;t always kick in even in cases it seemed applicable. The previous example showed loop cloning with <code>&lt;<\/code> in the iteration condition. For whatever reason, however, some developers used <code>!=<\/code>, and loop cloning didn&#8217;t apply (I&#8217;m guessing they used <code>!=<\/code> because they thought it was more efficient, and they actually end up deoptimizing). Thanks to <a href=\"https:\/\/github.com\/dotnet\/runtime\/pull\/129268\">dotnet\/runtime#129268<\/a>, in .NET 11 <code>!=<\/code> is now also handled, as long as specific conditions are met, such as the stride being exactly 1 or -1, e.g. <code>i++<\/code> qualifies, while <code>i += 2<\/code> doesn&#8217;t. <a href=\"https:\/\/github.com\/dotnet\/runtime\/pull\/129303\">dotnet\/runtime#129303<\/a> also improves loops that terminate with <code>i != bound<\/code>, giving the JIT a tighter understanding of the values <code>i<\/code> can take and allowing it to remove some bounds checks even when it can&#8217;t clone the whole loop.<\/p>\n<p>Lookahead in arrays and spans is another recurring pattern, especially in parsers. <a href=\"https:\/\/github.com\/dotnet\/runtime\/pull\/124242\">dotnet\/runtime#124242<\/a> and <a href=\"https:\/\/github.com\/dotnet\/runtime\/pull\/125235\">dotnet\/runtime#125235<\/a> recognize conditions such as <code>(uint)(i + 2) &lt; (uint)span.Length<\/code> and use that relation to remove the follow-on checks for <code>span[i + 1]<\/code> and <code>span[i + 2]<\/code>. Consider this benchmark:<\/p>\n<pre><code class=\"language-csharp\">\/\/ dotnet run -c Release -f net10.0 --filter \"*\" --runtimes net10.0 net11.0\r\n\r\nusing BenchmarkDotNet.Attributes;\r\nusing BenchmarkDotNet.Running;\r\nusing System;\r\nusing System.Linq;\r\n\r\nBenchmarkSwitcher.FromAssembly(typeof(Benchmarks).Assembly).Run(args);\r\n\r\n[HideColumns(\"Job\", \"Error\", \"StdDev\", \"Median\", \"RatioSD\")]\r\npublic class Benchmarks\r\n{\r\n    private readonly string _text = string.Concat(Enumerable.Repeat(\"%FE\", 128));\r\n\r\n    [Benchmark]\r\n    public bool ContainsPercentFF()\r\n    {\r\n        ReadOnlySpan&lt;char&gt; span = _text;\r\n        for (int i = 0; i &lt; span.Length; i++)\r\n        {\r\n            if (span[i] == '%' &amp;&amp;\r\n                (uint)(i + 2) &lt; (uint)span.Length &amp;&amp;\r\n                span[i + 1] == 'F' &amp;&amp;\r\n                span[i + 2] == 'F')\r\n            {\r\n                return true;\r\n            }\r\n        }\r\n\r\n        return false;\r\n    }\r\n}<\/code><\/pre>\n<table>\n<thead>\n<tr>\n<th>Method<\/th>\n<th>Runtime<\/th>\n<th>Mean<\/th>\n<th>Ratio<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td>ContainsPercentFF<\/td>\n<td>.NET 10.0<\/td>\n<td>232.1 ns<\/td>\n<td>1.00<\/td>\n<\/tr>\n<tr>\n<td>ContainsPercentFF<\/td>\n<td>.NET 11.0<\/td>\n<td>194.9 ns<\/td>\n<td>0.84<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<p>Several smaller changes broaden the range of code from which the JIT can remove bounds checks:<\/p>\n<ul>\n<li><a href=\"https:\/\/github.com\/dotnet\/runtime\/pull\/121640\">dotnet\/runtime#121640<\/a> helps in a situation where once an access using a chosen index has been checked, a later access to the same array at that index need not be checked again.<\/li>\n<li><a href=\"https:\/\/github.com\/dotnet\/runtime\/pull\/121683\">dotnet\/runtime#121683<\/a> enables the JIT to trace an array&#8217;s length through calculations performed earlier in the method, exposing more redundant checks, including some involving index-from-end expressions.<\/li>\n<li><a href=\"https:\/\/github.com\/dotnet\/runtime\/pull\/124387\">dotnet\/runtime#124387<\/a> and <a href=\"https:\/\/github.com\/dotnet\/runtime\/pull\/130326\">dotnet\/runtime#130326<\/a> teach the optimizer to rely on a span&#8217;s length always being non-negative.<\/li>\n<li><a href=\"https:\/\/github.com\/dotnet\/runtime\/pull\/124571\">dotnet\/runtime#124571<\/a> improves sequences of index-from-end accesses: once an access like <code>arr[^4]<\/code> establishes that the array has at least four elements, the JIT reuses that information for nearby accesses such as <code>arr[^3]<\/code>.<\/li>\n<li><a href=\"https:\/\/github.com\/dotnet\/runtime\/pull\/129101\">dotnet\/runtime#129101<\/a> improves how the JIT combines and carries forward the possible ranges of arithmetic expressions, including expressions involving bitwise OR and unsigned division. Those tighter ranges can show that more values are non-negative or within bounds.<\/li>\n<\/ul>\n<p>Bounds-check elimination is only one payoff from understanding a loop&#8217;s structure. The JIT analyzes induction variables (values like loop counters that change predictably each iteration) and puts loops into standard forms so that later optimizations can reason about them. .NET 11 broadens the range of loops for which that works:<\/p>\n<ul>\n<li><a href=\"https:\/\/github.com\/dotnet\/runtime\/pull\/122184\">dotnet\/runtime#122184<\/a> recognizes another representation of a 32-to-64-bit zero extension. That lets pointer loops using expressions such as <code>data[(uint)i]<\/code> replace the repeated index extension and address calculation with a pointer increment.<\/li>\n<li><a href=\"https:\/\/github.com\/dotnet\/runtime\/pull\/119537\">dotnet\/runtime#119537<\/a> follows simple control-flow predecessors when finding an induction variable&#8217;s initialization and zero-trip test, while <a href=\"https:\/\/github.com\/dotnet\/runtime\/pull\/128303\">dotnet\/runtime#128303<\/a> gives loops with multiple backedges a single canonical latch block.<\/li>\n<li><a href=\"https:\/\/github.com\/dotnet\/runtime\/pull\/128532\">dotnet\/runtime#128532<\/a> makes loop cloning tolerate more statements around the update and test.<\/li>\n<li><a href=\"https:\/\/github.com\/dotnet\/runtime\/pull\/129309\">dotnet\/runtime#129309<\/a> extends cloning to more span loops with non-unit strides and offset limits.<\/li>\n<li><a href=\"https:\/\/github.com\/dotnet\/runtime\/pull\/129349\">dotnet\/runtime#129349<\/a> handles large strides in array loops with an explicit safety guard rather than rejecting them outright.<\/li>\n<li><a href=\"https:\/\/github.com\/dotnet\/runtime\/pull\/129472\">dotnet\/runtime#129472<\/a> allows loop inversion to spend more of its budget on likely cloning candidates.<\/li>\n<li><a href=\"https:\/\/github.com\/dotnet\/runtime\/pull\/130205\">dotnet\/runtime#130205<\/a> removes comparisons that are redundant given the induction variable&#8217;s known range.<\/li>\n<li><a href=\"https:\/\/github.com\/dotnet\/runtime\/pull\/131362\">dotnet\/runtime#131362<\/a> corrects profile weights after inversion changes a loop&#8217;s exit.<\/li>\n<\/ul>\n<p>Much of this work wasn&#8217;t motivated by contrived benchmarks containing nothing\nbut array indexing as I&#8217;m prone to use in these posts. Rather, many of the improvements\nstemmed from an ongoing audit of unsafe code throughout the\n.NET libraries, part of a broader\n<a href=\"https:\/\/github.com\/dotnet\/designs\/blob\/main\/accepted\/2025\/memory-safety\/memory-safety.md\">effort to improve memory safety in .NET<\/a>. .NET and C# are memory safe, but as with other memory safe languages like Rust,\nit provides escape hatches that enable turning off the guardrails provided by the compiler and runtime.\nThis effort is about reducing where and when developers feel compelled to use those escape hatches, since every\noccurrence is an opportunity for increased risk. Unsafe code was often introduced years earlier to manually avoid bounds\nchecks, typically by walking a buffer with pointers, byrefs, or <code>Unsafe.Add<\/code>.\nSometimes the audit found that the unsafe code was no longer needed and could\nsimply be removed. Sometimes a &#8220;safe&#8221; rewrite (meaning not using <code>unsafe<\/code> and friends) was already just as fast or even faster.\nAnd sometimes the rewrite exposed an optimization the JIT was missing, in which\ncase the answer was to improve the JIT and then rewrite the library code to use\nnormal, bounds-checked C#. Several of the optimizations discussed in this\nsection are the result of exactly that feedback loop. <a href=\"https:\/\/github.com\/dotnet\/runtime\/pull\/127429\">dotnet\/runtime#127429<\/a> is a\nparticularly nice example. The vectorized implementation of <code>Enumerable.Sum<\/code>\nused <code>MemoryMarshal.GetReference<\/code>, <code>Vector.LoadUnsafe<\/code>, and <code>Unsafe.Add<\/code> to walk\nits input without bounds checks. With the span-slicing improvements described\nearlier, it could instead use <code>Vector.Create(span)<\/code>, <code>span.Slice(...)<\/code>, and a\n<code>foreach<\/code> for the tail. That&#8217;s easier to reason about, removes the unchecked\nindexing, and ended up being faster.\n<a href=\"https:\/\/github.com\/dotnet\/runtime\/pull\/114757\">dotnet\/runtime#114757<\/a> similarly\nreplaced an unsafe pointer-based header-name accessor with a generic\n<code>ReadOnlySpan&lt;T&gt;<\/code> implementation without loss of performance.\nSimilarly, <a href=\"https:\/\/github.com\/dotnet\/runtime\/pull\/121270\">dotnet\/runtime#121270<\/a> removed\nmore unsafe code from <code>Uri<\/code> parsing and actually improved performance of the cited code measurably.<\/p>\n<p>There&#8217;s a useful &#8220;go do&#8221; here for libraries outside of dotnet\/runtime, as well.\nUnsafe code written to work around the JIT is a snapshot of what the JIT could\ndo at the time that code was written. If you own code that has hand-written pointer or\n<code>Unsafe<\/code>-based loops whose purpose is to avoid bounds checks, it&#8217;s worth\nrewriting them with safe, bounds-checked C# and measuring again on .NET 11.\nChances are, you&#8217;ll find the gap at this point is either non-existent or small\nenough that it&#8217;s not worth the increased maintenance and risk for managing the safety yourself.\nAnd if the revised version is still slower, that&#8217;s a great opportunity for you to share a repro\nin the dotnet\/runtime repo, hopefully serving as inspiration for one of the first performance improvements\nto go into the JIT for .NET 12. <code>unsafe<\/code> code is still necessary for scenarios like interop,\nbut performance alone shouldn&#8217;t be a permanent reason to eschew all the valuable guardrails .NET provides.<\/p>\n<p>The <a href=\"https:\/\/learn.microsoft.com\/dotnet\/csharp\/whats-new\/csharp-15#memory-safety\">C# 15 memory-safety preview<\/a>\npushes in the same direction and is part and parcel of this effort. Historically, C# has largely equated pointers\nwith unsafe code: simply declaring or manipulating a pointer generally required\nan <code>unsafe<\/code> context, even if the code never accessed the memory to which it\npoints. In the preview, pointer plumbing such as declaring a pointer, taking an\naddress with <code>&amp;<\/code>, using <code>fixed<\/code>, converting <code>stackalloc<\/code> to a pointer, and\napplying <code>sizeof<\/code> to an unmanaged type no longer requires an <code>unsafe<\/code> context.\nOperations that actually access the pointed-to memory, including <code>*p<\/code>,\n<code>p-&gt;member<\/code>, and <code>p[i]<\/code>, still do. C# 15 also adds an <code>unsafe(expression)<\/code> form,\nanalogous to <code>checked(expression)<\/code>, so an unsafe context can cover one precise\nexpression rather than a larger statement block. Those changes are the first preview slice of a larger, multi-release\n<a href=\"https:\/\/github.com\/dotnet\/csharplang\/blob\/main\/proposals\/unsafe-evolution.md\">unsafe evolution<\/a>.\nThe end goal is to make unsafe regions smaller, make their assumptions visible through the call graph,\nand make them easier for reviewers and tools to find. Pairing that with a JIT\nthat makes idiomatic safe code fast removes a lot of the historical pressure to\nuse unsafe code in the first place.<\/p>\n<h3>Assertion Propagation<\/h3>\n<p>As discussed earlier, the JIT continually learns facts while compiling a method: a value equals a constant, a reference isn&#8217;t null, an integer falls within a particular range, and so on. &#8220;Assertion propagation&#8221; carries those facts forward so they can simplify later code. &#8220;Value numbering&#8221; complements it by letting the JIT recognize when two expressions compute the same value, even if they appear in different places or use different variables. Together, these mechanisms enable optimizations such as removing redundant null and bounds checks, folding conditions to constants, and reusing repeated computations. .NET 11 improves assertion propagation primarily by fixing places where useful facts were either never recorded or weren&#8217;t recognized later.<\/p>\n<p>For example, reading an array&#8217;s length normally carries an implicit null-check: if the array reference is <code>null<\/code>, the read must throw. Once global assertion propagation already knows the reference is non-null, however, we should be able to avoid the implicit null check. In .NET 11, <a href=\"https:\/\/github.com\/dotnet\/runtime\/pull\/124291\">dotnet\/runtime#124291<\/a> takes care of that for <code>Array.Length<\/code>:<\/p>\n<pre><code class=\"language-csharp\">\/\/ dotnet run -c Release -f net10.0 --filter \"*\" --runtimes net10.0 net11.0\r\n\r\nusing BenchmarkDotNet.Attributes;\r\nusing BenchmarkDotNet.Running;\r\nusing System.Runtime.CompilerServices;\r\n\r\nBenchmarkSwitcher.FromAssembly(typeof(Benchmarks).Assembly).Run(args);\r\n\r\n[DisassemblyDiagnoser, HideColumns(\"Job\", \"Error\", \"StdDev\", \"Median\", \"RatioSD\")]\r\npublic class Benchmarks\r\n{\r\n    private readonly int[] _values = new int[1024];\r\n\r\n    [Benchmark]\r\n    public void DeadLength() =&gt; Test(_values);\r\n\r\n    [MethodImpl(MethodImplOptions.NoInlining)]\r\n    private static void Test(int[]? values)\r\n    {\r\n        if (values is not null)\r\n            _ = values.Length;\r\n    }\r\n}<\/code><\/pre>\n<p>The .NET 10 code still tests the reference and reads the length. In .NET 11, the guard proves the read can&#8217;t throw, and since its result isn&#8217;t used, the access disappears:<\/p>\n<pre><code class=\"language-diff\">; Arm64\r\n--- .NET 10\r\n+++ .NET 11\r\n G_M000_IG01:\r\n             stp     fp, lr, [sp, #-0x10]!\r\n             mov     fp, sp\r\n\r\n G_M000_IG02:\r\n-            cbz     x0, G_M000_IG04\r\n-\r\n-G_M000_IG03:\r\n-            ldr     wzr, [x0, #0x08]\r\n-\r\n-G_M000_IG04:\r\n             ldp     fp, lr, [sp], #0x10\r\n             ret     lr\r\n\r\n-; Total bytes of code 24\r\n+; Total bytes of code 16<\/code><\/pre>\n<p><a href=\"https:\/\/github.com\/dotnet\/runtime\/pull\/119474\">dotnet\/runtime#119474<\/a> improves\nthe starting point for integer range analysis. The JIT now uses facts inherent\nin a value itself, e.g. a constant has one exact value, while a value converted\nto <code>byte<\/code>, for example, must be between 0 and 255. That can eliminate bounds\nchecks and conditions even when no preceding <code>if<\/code> explicitly established the\nrange. <a href=\"https:\/\/github.com\/dotnet\/runtime\/pull\/124415\">dotnet\/runtime#124415<\/a>\nfurther refines this handling of casts, combining what is known about both the\nsource value and the destination type to derive the tightest useful range.<\/p>\n<p>Those improvements derive ranges from facts inherent in a value, but ranges\ncan also come from control flow. After <code>if ((uint)x &lt; 10)<\/code>, for example, the\nJIT knows that <code>x<\/code> is between 0 and 9 on the true path, which may be enough to\nremove a later comparison or array bounds check.\n<a href=\"https:\/\/github.com\/dotnet\/runtime\/pull\/123624\">dotnet\/runtime#123624<\/a>\nderives tighter ranges from assertions and casts, including proving that some\ncomparisons are always true or false.\n<a href=\"https:\/\/github.com\/dotnet\/runtime\/pull\/129390\">dotnet\/runtime#129390<\/a>\npreserves range information more accurately when control-flow paths merge.<\/p>\n<p>Other changes make better use of the ranges once known. <a href=\"https:\/\/github.com\/dotnet\/runtime\/pull\/129354\">dotnet\/runtime#129354<\/a> traces values back through their definitions to fold more span- and slice-related comparisons, and <a href=\"https:\/\/github.com\/dotnet\/runtime\/pull\/126917\">dotnet\/runtime#126917<\/a> uses narrowed ranges to remove more relational branches.<\/p>\n<p><a href=\"https:\/\/github.com\/dotnet\/runtime\/pull\/124711\">dotnet\/runtime#124711<\/a> teaches the JIT to learn implicit facts from operations that have already completed successfully. For example:<\/p>\n<ul>\n<li>Creating an array proves its requested length wasn&#8217;t negative.<\/li>\n<li>A reference-array store may need a runtime covariance check, because a value typed as <code>object[]<\/code> can actually refer to a <code>string[]<\/code>; the helper that performs that type check also validates the index, so if it returns successfully, the index was in range.<\/li>\n<li>Integer division or modulo proves the divisor wasn&#8217;t zero.<\/li>\n<\/ul>\n<p>And so on. Those facts can then remove redundant checks and conditions later in the method.<\/p>\n<pre><code class=\"language-csharp\">\/\/ dotnet run -c Release -f net10.0 --filter \"*\" --runtimes net10.0 net11.0\r\n\r\nusing BenchmarkDotNet.Attributes;\r\nusing BenchmarkDotNet.Running;\r\n\r\nBenchmarkSwitcher.FromAssembly(typeof(Benchmarks).Assembly).Run(args);\r\n\r\n[HideColumns(\"Job\", \"Error\", \"StdDev\", \"Median\", \"RatioSD\")]\r\npublic class Benchmarks\r\n{\r\n    private readonly object?[] _objArr = new object?[8];\r\n    private readonly object _value = new();\r\n\r\n    [Benchmark]\r\n    public object? CovariantArrayStore()\r\n    {\r\n        object?[] objArr = _objArr;\r\n        objArr[3] = _value;\r\n        return objArr[2];\r\n    }\r\n}<\/code><\/pre>\n<p>A successful store to element 3 proves that particular array has at least four elements; since an array&#8217;s length can&#8217;t change, the subsequent read of element 2 doesn&#8217;t need another bounds check.<\/p>\n<table>\n<thead>\n<tr>\n<th>Method<\/th>\n<th>Runtime<\/th>\n<th>Mean<\/th>\n<th>Ratio<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td>CovariantArrayStore<\/td>\n<td>.NET 10.0<\/td>\n<td>3.565 ns<\/td>\n<td>1.00<\/td>\n<\/tr>\n<tr>\n<td>CovariantArrayStore<\/td>\n<td>.NET 11.0<\/td>\n<td>2.985 ns<\/td>\n<td>0.84<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<p><a href=\"https:\/\/github.com\/dotnet\/runtime\/pull\/128522\">dotnet\/runtime#128522<\/a> simplifies how the global assertion pass identifies values, making it less likely to miss a fact learned earlier. One practical impact of this is better propagation of a static <code>string<\/code>&#8216;s known length, which can turn a general string comparison into a fixed-size vectorized comparison.<\/p>\n<p><a href=\"https:\/\/github.com\/dotnet\/runtime\/pull\/127810\">dotnet\/runtime#127810<\/a> improves null-check elimination where control flow merges. With <code>??=<\/code>, which is a very common operator used for lazy initialization, the resulting value is non-null whether it came from the existing field or from the newly allocated object. The JIT now combines the facts from both paths and recognizes that the subsequent call doesn&#8217;t need another null check.<\/p>\n<pre><code class=\"language-csharp\">\/\/ dotnet run -c Release -f net10.0 --filter \"*\" --runtimes net10.0 net11.0\r\n\r\nusing BenchmarkDotNet.Attributes;\r\nusing BenchmarkDotNet.Running;\r\nusing System.Runtime.CompilerServices;\r\n\r\nBenchmarkSwitcher.FromAssembly(typeof(Benchmarks).Assembly).Run(args);\r\n\r\n[DisassemblyDiagnoser, HideColumns(\"Job\", \"Error\", \"StdDev\", \"Median\", \"RatioSD\")]\r\npublic class Benchmarks\r\n{\r\n    Inner? _inner;\r\n\r\n    [Benchmark]\r\n    [Arguments(42)]\r\n    public int Invoke(int n) =&gt; (_inner ??= new()).Increment(n);\r\n\r\n    private sealed class Inner\r\n    {\r\n        [MethodImpl(MethodImplOptions.NoInlining)]\r\n        public int Increment(int n) =&gt; n + 1;\r\n    }\r\n}<\/code><\/pre>\n<p>The generated code consequently loses the null check on the merged value:<\/p>\n<pre><code class=\"language-diff\">; x64\r\n M00_L00:\r\n        mov      edx, esi\r\n-       cmp      [rcx], ecx\r\n        call     qword ptr [...] ; Inner.Increment(Int32)\r\n\r\n-; Total bytes of code 75\r\n+; Total bytes of code 73<\/code><\/pre>\n<p><a href=\"https:\/\/github.com\/dotnet\/runtime\/pull\/128701\">dotnet\/runtime#128701<\/a> removes similarly redundant null checks from copies of structs that contain object references. Such copies use a runtime helper so the garbage collector is correctly notified about the reference writes, but lowering had been adding probes for both source and destination without preserving whether either address could actually fault. It now emits only the probes that are needed.<\/p>\n<pre><code class=\"language-csharp\">\/\/ dotnet run -c Release -f net10.0 --filter \"*\" --runtimes net10.0 net11.0\r\n\r\nusing BenchmarkDotNet.Attributes;\r\nusing BenchmarkDotNet.Running;\r\n\r\nBenchmarkSwitcher.FromAssembly(typeof(Benchmarks).Assembly).Run(args);\r\n\r\n[DisassemblyDiagnoser, HideColumns(\"Job\", \"Error\", \"StdDev\", \"Median\", \"RatioSD\")]\r\npublic class Benchmarks\r\n{\r\n    private FourRefs _src = new()\r\n    {\r\n        A = new(),\r\n        B = new(),\r\n        C = new(),\r\n        D = new()\r\n    };\r\n    private FourRefs _dst;\r\n\r\n    [Benchmark]\r\n    public void BulkStructCopy() =&gt; _dst = _src;\r\n\r\n    private struct FourRefs\r\n    {\r\n        public object? A;\r\n        public object? B;\r\n        public object? C;\r\n        public object? D;\r\n    }\r\n}<\/code><\/pre>\n<p>Both <code>_src<\/code> and <code>_dst<\/code> are fields of the same object, so after probing the source address has established that the object isn&#8217;t null, probing the destination address can&#8217;t provide any additional information. .NET 11 removes that second probe:<\/p>\n<pre><code class=\"language-diff\">; Arm64\r\n G_M000_IG02:\r\n             add     x1, x0, #8\r\n             ldrsb   wzr, [x1]\r\n             add     x0, x0, #40\r\n-            ldrsb   wzr, [x0]\r\n             movz    x2, ...\r\n             ldr     x3, [x2]\r\n             mov     x2, #32\r\n             blr     x3      \/\/ CORINFO_HELP_BULK_WRITEBARRIER\r\n\r\n-; Total bytes of code 56\r\n+; Total bytes of code 52<\/code><\/pre>\n<p>Additionally, <a href=\"https:\/\/github.com\/dotnet\/runtime\/pull\/125215\">dotnet\/runtime#125215<\/a> lets the JIT retain and efficiently find more assertions in larger methods, increasing the opportunities for the same kinds of simplification. And <a href=\"https:\/\/github.com\/dotnet\/runtime\/pull\/129312\">dotnet\/runtime#129312<\/a> removes unnecessary temporary variables when the same simple field address is used multiple times, enabling more efficient loads and stores.<\/p>\n<h3>Simplification<\/h3>\n<p>Assertion propagation is largely about proving things to help the generated code. Once the JIT knows enough about an operation&#8217;s inputs, it can often replace the operation with something simpler and cheaper.<\/p>\n<p>&#8220;Constant folding&#8221; is a fancy way of saying the compiler does work once so it doesn&#8217;t need to be repeated at run time. If the compiler has everything it needs to compute an answer when building, it can bake that answer in to the generated code and avoid needing the code to re-compute it. That answer can then be further used by other computations at build time, potentially folding further. The C# compiler handles constant folding expressions composed entirely of language constants, while the JIT compiler can go further after inlining and after learning things about values and control flow. The JIT already does a ton of folding, and as with every release, it goes further in .NET 11.<\/p>\n<p>One straightforward example is the offset of a field within a struct. <a href=\"https:\/\/github.com\/dotnet\/runtime\/pull\/122297\">dotnet\/runtime#122297<\/a> recognizes more cases where two addresses refer to the same struct and replaces their difference with the known field offset. Here, the second <code>int<\/code> field begins four bytes into the struct:<\/p>\n<pre><code class=\"language-csharp\">\/\/ dotnet run -c Release -f net10.0 --filter \"*\" --runtimes net10.0 net11.0\r\n\r\nusing BenchmarkDotNet.Attributes;\r\nusing BenchmarkDotNet.Running;\r\nusing System.Runtime.CompilerServices;\r\n\r\nBenchmarkSwitcher.FromAssembly(typeof(Benchmarks).Assembly).Run(args);\r\n\r\n[DisassemblyDiagnoser, HideColumns(\"Job\", \"Error\", \"StdDev\", \"Median\", \"RatioSD\")]\r\npublic unsafe class Benchmarks\r\n{\r\n    private struct MyStruct\r\n    {\r\n        public int A;\r\n        public int Field;\r\n    }\r\n\r\n    [MethodImpl(MethodImplOptions.AggressiveInlining)]\r\n    private static nint OffsetOfFieldInline()\r\n    {\r\n        MyStruct dummy;\r\n        return (nint)((byte*)&amp;dummy.Field - (byte*)&amp;dummy);\r\n    }\r\n\r\n    [Benchmark]\r\n    [Arguments(1_000)]\r\n    public nint OffsetOfFieldLoop(int n)\r\n    {\r\n        nint sum = 0;\r\n        for (int i = 0; i &lt; n; i++)\r\n            sum += OffsetOfFieldInline();\r\n\r\n        return sum;\r\n    }\r\n\r\n}<\/code><\/pre>\n<p>Without the fold, the loop repeatedly computes the field offset. With the fold, each iteration simply adds the constant <code>4<\/code>.<\/p>\n<pre><code class=\"language-diff\">; Arm64\r\n--- .NET 10\r\n+++ .NET 11\r\n@@ -1,7 +1,6 @@\r\n G_M000_IG01:\r\n-            stp     fp, lr, [sp, #-0x20]!\r\n+            stp     fp, lr, [sp, #-0x10]!\r\n             mov     fp, sp\r\n-            str     xzr, [fp, #0x18]\r\n\r\n G_M000_IG02:\r\n             mov     x0, xzr\r\n\r\n@@ -9,22 +8,18 @@\r\n             ble     G_M000_IG05\r\n\r\n G_M000_IG03:\r\n-            add     x2, fp, #0x1C\r\n-            add     x3, fp, #24\r\n-            sub     x2, x2, x3\r\n             align   [0 bytes for IG04]\r\n             align   [0 bytes]\r\n             align   [0 bytes]\r\n             align   [0 bytes]\r\n\r\n G_M000_IG04:\r\n-            str     xzr, [fp, #0x18]\r\n-            add     x0, x2, x0\r\n+            add     x0, x0, #4\r\n             sub     w1, w1, #1\r\n             cbnz    w1, G_M000_IG04\r\n\r\n G_M000_IG05:\r\n-            ldp     fp, lr, [sp], #0x20\r\n+            ldp     fp, lr, [sp], #0x10\r\n             ret     lr\r\n\r\n-; Total bytes of code 60\r\n+; Total bytes of code 40<\/code><\/pre>\n<p><a href=\"https:\/\/github.com\/dotnet\/runtime\/pull\/121985\">dotnet\/runtime#121985<\/a> from <a href=\"https:\/\/github.com\/hez2010\">@hez2010<\/a> enables the JIT to evaluate <code>SequenceEqual<\/code> at compile time when both inputs are known. <code>SequenceEqual<\/code> normally walks two sequences element by element, stopping at the first mismatch. But if inlining exposes both sequences as constants, there&#8217;s nothing useful left to do at run time: the JIT can compare them while compiling and replace the whole operation with a constant <code>true<\/code> or <code>false<\/code>. This intrinsic underpins APIs including <code>MemoryExtensions.SequenceEqual<\/code>, <code>ReadOnlySpan&lt;T&gt;.SequenceEqual<\/code>, and <code>string.Equals<\/code>.<\/p>\n<pre><code class=\"language-csharp\">\/\/ dotnet run -c Release -f net10.0 --filter \"*\" --runtimes net10.0 net11.0\r\n\r\nusing BenchmarkDotNet.Attributes;\r\nusing BenchmarkDotNet.Running;\r\n\r\nBenchmarkSwitcher.FromAssembly(typeof(Benchmarks).Assembly).Run(args);\r\n\r\n[DisassemblyDiagnoser, HideColumns(\"Job\", \"Error\", \"StdDev\", \"Median\", \"RatioSD\")]\r\npublic class Benchmarks\r\n{\r\n    private static string AlphaLower =&gt; \"abcdefghijklmnopqrstuvwxyz\";\r\n    private static string AlphaUpper =&gt; \"ABCDEFGHIJKLMNOPQRSTUVWXYZ\";\r\n\r\n    [Benchmark]\r\n    public bool CompareEqual() =&gt; AlphaLower.Equals(AlphaLower);\r\n\r\n    [Benchmark]\r\n    public bool CompareDistinct() =&gt; AlphaLower.Equals(AlphaUpper);\r\n}<\/code><\/pre>\n<p>Because these properties aren&#8217;t <code>const<\/code>, the C# compiler can&#8217;t evaluate the comparisons. The JIT, however, can see the string literals after inlining. It now folds comparisons of the same input whose contents are available at the time of compilation. <code>CompareDistinct<\/code> therefore becomes a constant <code>false<\/code>.<\/p>\n<pre><code class=\"language-diff\">; x64\r\n--- .NET 10\r\n+++ .NET 11\r\n-mov       rax,LOWER_STRING\r\n-mov       rcx,UPPER_STRING\r\n-add       rax,0C\r\n-vmovups   ymm0,[rax]\r\n-vmovups   ymm1,[rax+14]\r\n-vmovups   ymm2,[rcx]\r\n-vpxor     ymm0,ymm2,ymm0\r\n-vpxor     ymm1,ymm1,[rcx+14]\r\n-vpor      ymm0,ymm1,ymm0\r\n-vptest    ymm0,ymm0\r\n-sete      al\r\n-movzx     eax,al\r\n-vzeroupper\r\n+xor       eax,eax\r\n ret\r\n\r\n-; Total bytes of code 65\r\n+; Total bytes of code 3<\/code><\/pre>\n<p>Folding an operation is only the first step, though. The result can then simplify later code, even when it&#8217;s a vector. <a href=\"https:\/\/github.com\/dotnet\/runtime\/pull\/127124\">dotnet\/runtime#127124<\/a> extends assertion propagation to 128-bit integer vector constants. If a branch establishes that a vector is zero, uses of that vector within the branch can now be replaced with zero and simplified just like scalar values.<\/p>\n<pre><code class=\"language-csharp\">\/\/ dotnet run -c Release -f net10.0 --filter \"*\" --runtimes net10.0 net11.0\r\n\r\nusing BenchmarkDotNet.Attributes;\r\nusing BenchmarkDotNet.Running;\r\nusing System.Runtime.CompilerServices;\r\nusing System.Runtime.Intrinsics;\r\n\r\nBenchmarkSwitcher.FromAssembly(typeof(Benchmarks).Assembly).Run(args);\r\n\r\n[DisassemblyDiagnoser, HideColumns(\"Job\", \"Error\", \"StdDev\", \"Median\", \"RatioSD\")]\r\npublic class Benchmarks\r\n{\r\n    private int _selector;\r\n\r\n    [MethodImpl(MethodImplOptions.NoInlining)]\r\n    private Vector128&lt;int&gt; Compute() =&gt; _selector == 0 ? Vector128&lt;int&gt;.Zero : Vector128.Create(7);\r\n\r\n    [Benchmark]\r\n    public int AndNotIfZero()\r\n    {\r\n        Vector128&lt;int&gt; v = Compute();\r\n        if (v == Vector128&lt;int&gt;.Zero)\r\n        {\r\n            Vector128&lt;int&gt; masked = Vector128.AndNot(v, Vector128.Create(0x00FF00FF));\r\n            return masked[0];\r\n        }\r\n\r\n        return -1;\r\n    }\r\n}<\/code><\/pre>\n<p>In the benchmark&#8217;s zero branch, the JIT can now fold away the mask creation, <code>AndNot<\/code>, and lane extraction, reducing the Arm64 method from 68 bytes to 56 bytes. This currently applies to integer vectors up to 128 bits (floating-point equality has additional NaN and signed-zero semantics that prevent the same reasoning at present).<\/p>\n<pre><code class=\"language-diff\">; Arm64\r\n--- .NET 10\r\n+++ .NET 11\r\n@@ -11,14 +11,11 @@\r\n             umaxp   v16.4s, v0.4s, v0.4s\r\n             umov    x0, v16.d[0]\r\n             movn    w1, #0\r\n-            movi    v16.8h, #0xFF,  LSL #8\r\n-            and     v16.4s, v0.4s, v16.4s\r\n-            smov    x2, v16.s[0]\r\n             cmp     x0, #0\r\n-            csel    w0, w1, w2, ne\r\n+            cinc    w0, w1, eq\r\nG_M000_IG03:\r\n             ldp     fp, lr, [sp], #0x10\r\n             ret     lr\r\n-; Total bytes of code 68\r\n+; Total bytes of code 56<\/code><\/pre>\n<p>Two backend cleanups take advantage of simpler expressions. <a href=\"https:\/\/github.com\/dotnet\/runtime\/pull\/124332\">dotnet\/runtime#124332<\/a> from <a href=\"https:\/\/github.com\/jonathandavies-arm\">@jonathandavies-arm<\/a> removes an unnecessary negation when Arm64 code compares a negated value with zero. And <a href=\"https:\/\/github.com\/dotnet\/runtime\/pull\/124642\">dotnet\/runtime#124642<\/a> from <a href=\"https:\/\/github.com\/yykkibbb\">@yykkibbb<\/a> lets short-circuit Boolean returns fold even when inlining has left unused writes in the same block; those stores previously obscured the simple Boolean expression from the optimizer.<\/p>\n<p>Branches offer another opportunity for simplification. Modern processors work on several instructions at different stages at the same time. When a processor encounters a conditional branch, it predicts which path will be taken so that it can continue fetching and executing instructions speculatively. A correct prediction hides much of the branch&#8217;s cost. A misprediction throws away that speculative work, redirects instruction fetch to the correct path, and refills the processor&#8217;s execution pipeline. That can make the predictability of a branch as important as the work in either branch. The JIT can sometimes avoid that variability, particularly inside small hot loops, by replacing a branch with a conditional move instruction or by recognizing that several branches describe one simpler condition. This isn&#8217;t always profitable: branchless code may evaluate work that a predictable branch would skip, making the branching code less expensive in the majority case. But it can be valuable for small, data-dependent choices.<\/p>\n<p><a href=\"https:\/\/github.com\/dotnet\/runtime\/pull\/124567\">dotnet\/runtime#124567<\/a>\nrecognizes zero-based equality chains, e.g.\n<code>value == 0 || value == 1 || value == 2<\/code>. Such chains can be replaced with an\nunsigned range check, e.g. <code>(uint)value &lt;= 2<\/code>, producing a branchless result.\nThe unsigned comparison also handles negative inputs: when interpreted as\nunsigned, any negative <code>int<\/code> is larger than the upper bound.<\/p>\n<pre><code class=\"language-csharp\">\/\/ dotnet run -c Release -f net10.0 --filter \"*\" --runtimes net10.0 net11.0\r\n\r\nusing BenchmarkDotNet.Attributes;\r\nusing BenchmarkDotNet.Running;\r\n\r\nBenchmarkSwitcher.FromAssembly(typeof(Benchmarks).Assembly).Run(args);\r\n\r\n[DisassemblyDiagnoser]\r\n[HideColumns(\"Job\", \"Error\", \"StdDev\", \"Median\", \"RatioSD\")]\r\npublic class Benchmarks\r\n{\r\n    private int _value = 2;\r\n\r\n    [Benchmark]\r\n    public bool IsLetterCategory() =&gt;\r\n        _value == 0 ||\r\n        _value == 1 ||\r\n        _value == 2 ||\r\n        _value == 3 ||\r\n        _value == 4;\r\n}<\/code><\/pre>\n<p>The .NET 10 JIT already combines the first four comparisons, but still needs\na branch and a separate comparison for <code>4<\/code>:<\/p>\n<pre><code class=\"language-x86asm\">; x64\r\nmov       ecx,[rcx+8]\r\ncmp       ecx,3\r\nja        CHECK_FOUR\r\nmov       eax,1\r\nret\r\n\r\nCHECK_FOUR:\r\ncmp       ecx,4\r\nsete      al\r\nmovzx     eax,al\r\nret<\/code><\/pre>\n<p>.NET 11 recognizes the whole chain as one unsigned range check, reducing the\nmethod from 24 bytes to 13:<\/p>\n<pre><code class=\"language-x86asm\">; x64\r\nmov       eax,[rcx+8]\r\ncmp       eax,5\r\nsetb      al\r\nmovzx     eax,al\r\nret<\/code><\/pre>\n<p><a href=\"https:\/\/github.com\/dotnet\/runtime\/pull\/128524\">dotnet\/runtime#128524<\/a> from <a href=\"https:\/\/github.com\/BoyBaykiller\">@BoyBaykiller<\/a> extends the same optimization to contiguous ranges that don&#8217;t start at zero. For example, <code>x == 3 || x == 4 || x == 5<\/code> can become <code>(uint)(x - 3) &lt;= 2<\/code>.<\/p>\n<p>Casts can obscure an equally simple comparison. <a href=\"https:\/\/github.com\/dotnet\/runtime\/pull\/128091\">dotnet\/runtime#128091<\/a> from <a href=\"https:\/\/github.com\/BoyBaykiller\">@BoyBaykiller<\/a> broadens cast-comparison optimization to equality and inequality. In this benchmark, converting a <code>uint<\/code> to <code>ulong<\/code> adds no information needed to compare it with <code>uint.MaxValue<\/code>, so the JIT can keep the comparison at 32 bits:<\/p>\n<pre><code class=\"language-csharp\">\/\/ dotnet run -c Release -f net10.0 --filter \"*\" --runtimes net10.0 net11.0\r\n\r\nusing BenchmarkDotNet.Attributes;\r\nusing BenchmarkDotNet.Running;\r\nusing System.Linq;\r\nusing System.Runtime.CompilerServices;\r\n\r\nBenchmarkSwitcher.FromAssembly(typeof(Benchmarks).Assembly).Run(args);\r\n\r\n[DisassemblyDiagnoser, HideColumns(\"Job\", \"Error\", \"StdDev\", \"Median\", \"RatioSD\")]\r\npublic class Benchmarks\r\n{\r\n    private readonly int[] _values = Enumerable.Range(0, 128).ToArray();\r\n\r\n    [Benchmark]\r\n    public int CastEquality()\r\n    {\r\n        int matches = 0;\r\n        foreach (int value in _values)\r\n            if ((ulong)(uint)value == uint.MaxValue)\r\n                matches++;\r\n\r\n        return matches;\r\n    }\r\n}<\/code><\/pre>\n<p>The widening cast disappears, reducing the Arm64 method from 80 bytes to 76 bytes.<\/p>\n<pre><code class=\"language-diff\">; Arm64\r\n--- .NET 10\r\n+++ .NET 11\r\n@@ -18,8 +18,7 @@\r\n\r\n G_M000_IG04:\r\n             ldr     w3, [x0]\r\n-            mov     x4, #0xFFFFFFFF\r\n-            cmp     x3, x4\r\n+            cmn     w3, #1\r\n             beq     G_M000_IG08\r\n\r\n G_M000_IG05:\r\n@@ -38,4 +37,4 @@\r\n             add     w1, w1, #1\r\n             b       G_M000_IG05\r\n\r\n-; Total bytes of code 80\r\n+; Total bytes of code 76<\/code><\/pre>\n<p>The examples thus far simplify individual comparisons. <a href=\"https:\/\/github.com\/dotnet\/runtime\/pull\/127181\">dotnet\/runtime#127181<\/a> also combines multiple comparisons in the same expression. For example, <code>(x &gt;= c) &amp;&amp; (x &lt;= c)<\/code> can only be true when <code>x == c<\/code>; corresponding OR forms can be simplified similarly.<\/p>\n<p>Once the JIT can reason about one comparison in terms of another, it can apply the same idea across branches. <a href=\"https:\/\/github.com\/dotnet\/runtime\/pull\/126587\">dotnet\/runtime#126587<\/a> removes an earlier test when a later, stronger test subsumes it. For example, <code>if (x &gt; 0) if (x &gt; 1)<\/code> needs only the <code>x &gt; 1<\/code> test, as reaching the nested body with <code>x &gt; 1<\/code> necessarily also means <code>x &gt; 0<\/code>.<\/p>\n<p>Rather than simply removing a test, the JIT can sometimes use the outcome of an earlier branch to choose the destination of a later one. This is known as &#8220;jump threading&#8221;: the JIT threads a control-flow path through the intervening jumps directly to its eventual destination. For example, consider:<\/p>\n<pre><code class=\"language-csharp\">int value = condition ? 1 : 2;\r\nif (value == 1)\r\n{\r\n    One();\r\n}\r\nelse\r\n{\r\n    Two();\r\n}<\/code><\/pre>\n<p>The path where <code>condition<\/code> is true can go directly to <code>One<\/code>, while the false path can go directly to <code>Two<\/code>, eliminating the second test, effectively:<\/p>\n<pre><code class=\"language-csharp\">int value;\r\nif (condition)\r\n{\r\n    value = 1;\r\n    One();\r\n}\r\nelse\r\n{\r\n    value = 2;\r\n    Two();\r\n}<\/code><\/pre>\n<p><a href=\"https:\/\/github.com\/dotnet\/runtime\/pull\/126812\">dotnet\/runtime#126812<\/a> lets this continue through more places where paths rejoin, and <a href=\"https:\/\/github.com\/dotnet\/runtime\/pull\/127103\">dotnet\/runtime#127103<\/a> ensures the rewritten values remain correct in more of those cases. <a href=\"https:\/\/github.com\/dotnet\/runtime\/pull\/127950\">dotnet\/runtime#127950<\/a> carries relationships between values further, so facts like <code>a &gt; 10<\/code> and <code>b &gt; a<\/code> can simplify later branches or bounds. The same reasoning can apply to type information. <a href=\"https:\/\/github.com\/dotnet\/runtime\/pull\/128500\">dotnet\/runtime#128500<\/a> combines the known types of instances arriving from multiple paths; if every value derives from the tested base type, the JIT can remove the <code>is<\/code> test after the paths merge. And <a href=\"https:\/\/github.com\/dotnet\/runtime\/pull\/127434\">dotnet\/runtime#127434<\/a> from <a href=\"https:\/\/github.com\/hez2010\">@hez2010<\/a> lets redundant-branch elimination look through empty jump blocks. Such a block contains no work of its own and exists only to redirect control elsewhere, but it could still hide the relationship between two conditions from the optimizer. Consider this benchmark:<\/p>\n<pre><code class=\"language-csharp\">\/\/ dotnet run -c Release -f net10.0 --filter \"*\" --runtimes net10.0 net11.0\r\n\r\nusing BenchmarkDotNet.Attributes;\r\nusing BenchmarkDotNet.Running;\r\nusing System.Runtime.CompilerServices;\r\n\r\nBenchmarkSwitcher.FromAssembly(typeof(Benchmarks).Assembly).Run(args);\r\n\r\n[DisassemblyDiagnoser, HideColumns(\"Job\", \"Error\", \"StdDev\", \"Median\", \"RatioSD\")]\r\npublic class Benchmarks\r\n{\r\n    private static object? s_sink;\r\n\r\n    private int _x = 20;\r\n    private int _y = 30;\r\n    private bool _flag = true;\r\n    private int _count = 5;\r\n\r\n    [Benchmark]\r\n    public bool TransitiveComparison() =&gt; TransitiveComparison(_x, _y);\r\n\r\n    [Benchmark]\r\n    public bool MergedTypeCheck() =&gt; MergedTypeCheck(_flag);\r\n\r\n    [Benchmark]\r\n    public int NestedThresholds() =&gt; NestedThresholds(_count);\r\n\r\n    [MethodImpl(MethodImplOptions.NoInlining)]\r\n    private static bool TransitiveComparison(int x, int y)\r\n    {\r\n        if (x &gt; 10 &amp;&amp; x &lt; 100 &amp;&amp; y &gt; x)\r\n            return y &gt; 0;\r\n\r\n        return false;\r\n    }\r\n\r\n    [MethodImpl(MethodImplOptions.NoInlining)]\r\n    private static bool MergedTypeCheck(bool flag)\r\n    {\r\n        object shape = flag ? new Circle() : new Rectangle();\r\n        s_sink = shape;\r\n        return shape is Shape;\r\n    }\r\n\r\n    [MethodImpl(MethodImplOptions.NoInlining)]\r\n    private static int NestedThresholds(int count)\r\n    {\r\n        if (count &gt; 1)\r\n            if (count &gt; 2)\r\n                if (count &gt; 3)\r\n                    if (count &gt; 4)\r\n                        return 1;\r\n\r\n        return 3;\r\n    }\r\n\r\n    private abstract class Shape;\r\n    private sealed class Circle : Shape;\r\n    private sealed class Rectangle : Shape;\r\n}<\/code><\/pre>\n<p>In <code>TransitiveComparison<\/code>, reaching <code>y &gt; 0<\/code> means the JIT already knows that <code>x &gt; 10<\/code> and <code>y &gt; x<\/code>, which together prove that <code>y<\/code> is positive. The final comparison disappears, reducing the Arm64 method from 40 bytes to 36 bytes:<\/p>\n<pre><code class=\"language-diff\">; Arm64\r\n--- .NET 10\r\n+++ .NET 11\r\n             cmp     w1, w0\r\n             ccmp    w2, w3, c, gt\r\n-            ccmp    w1, #0, nzc, ls\r\n-            cset    x0, gt\r\n+            cset    x0, ls\r\n\r\n-; Total bytes of code 40\r\n+; Total bytes of code 36<\/code><\/pre>\n<p>In <code>MergedTypeCheck<\/code>, each path creates a different concrete type, but both derive from <code>Shape<\/code>. .NET 11 keeps the allocations and the store that make the example observable, but replaces the <code>is<\/code> helper call and its result test with the constant <code>true<\/code>, reducing the method from 112 bytes to 88 bytes:<\/p>\n<pre><code class=\"language-diff\">; Arm64\r\n--- .NET 10\r\n+++ .NET 11\r\n             bl      CORINFO_HELP_ASSIGN_REF\r\n-            movz    x0, #0xEA30\r\n-            movk    x0, #0x4EB LSL #16\r\n-            movk    x0, #0x7FFF LSL #32\r\n-            bl      CORINFO_HELP_ISINSTANCEOFCLASS\r\n-            cmp     x0, #0\r\n-            cset    x0, ne\r\n+            mov     w0, #1\r\n\r\n-; Total bytes of code 112\r\n+; Total bytes of code 88<\/code><\/pre>\n<p>For <code>NestedThresholds<\/code>, reaching the <code>return 1<\/code> requires <code>count<\/code> to be greater than all four constants, which is equivalent to just <code>count &gt; 4<\/code>. Once redundant-branch elimination can see through the empty jump blocks left behind while simplifying the nested conditions, the other three comparisons disappear:<\/p>\n<pre><code class=\"language-diff\">; Arm64\r\n--- .NET 10\r\n+++ .NET 11\r\n             mov     w1, #3\r\n             mov     w2, #1\r\n-            cmp     w0, #1\r\n-            ccmp    w0, #2, nzc, gt\r\n-            ccmp    w0, #3, nzc, gt\r\n-            ccmp    w0, #4, nzc, gt\r\n+            cmp     w0, #4\r\n             csel    w0, w1, w2, le\r\n\r\n-; Total bytes of code 44\r\n+; Total bytes of code 32<\/code><\/pre>\n<p>Removing a redundant branch is ideal; why do work when it&#8217;s provably unnecessary? Often, however, the branch is necessary, as both outcomes are possible (or at least not provably impossible). In such cases, the JIT may still be able to avoid branching via specialized instructions that bake the choice into the instruction. &#8220;If-conversion&#8221; replaces a small <code>if<\/code>\/<code>else<\/code> with a conditional-move instruction or another branchless form when both alternatives are cheap. The JIT has been able to do this for several releases, and improves in .NET 11. <a href=\"https:\/\/github.com\/dotnet\/runtime\/pull\/124738\">dotnet\/runtime#124738<\/a> from <a href=\"https:\/\/github.com\/BoyBaykiller\">@BoyBaykiller<\/a> recognizes an earlier default assignment as the implicit <code>else<\/code>, so <code>bool x = false; if (cond) x = true;<\/code> can become the same branchless form as an explicit <code>else<\/code>. <a href=\"https:\/\/github.com\/dotnet\/runtime\/pull\/127915\">dotnet\/runtime#127915<\/a> from <a href=\"https:\/\/github.com\/BoyBaykiller\">@BoyBaykiller<\/a> handles the opposite cleanup, removing a conditional selection when both outcomes are the same constant while preserving any side effects from evaluating the condition. <a href=\"https:\/\/github.com\/dotnet\/runtime\/pull\/128533\">dotnet\/runtime#128533<\/a> from <a href=\"https:\/\/github.com\/BoyBaykiller\">@BoyBaykiller<\/a> also helps these Boolean optimizations meet in the middle by normalizing power-of-two bit tests. A power of two has exactly one bit set, in which case <code>(A &amp; bit) == bit<\/code> is equivalent to <code>(A &amp; bit) != 0<\/code>; putting both forms into the same canonical representation makes them easier to combine with surrounding conditions. All three improvements are visible in the following benchmarks:<\/p>\n<pre><code class=\"language-csharp\">\/\/ dotnet run -c Release -f net10.0 --filter \"*\" --runtimes net10.0 net11.0\r\n\r\nusing BenchmarkDotNet.Attributes;\r\nusing BenchmarkDotNet.Running;\r\nusing System.Runtime.CompilerServices;\r\n\r\nBenchmarkSwitcher.FromAssembly(typeof(Benchmarks).Assembly).Run(args);\r\n\r\n[DisassemblyDiagnoser, HideColumns(\"Job\", \"Error\", \"StdDev\", \"Median\", \"RatioSD\")]\r\npublic class Benchmarks\r\n{\r\n    private int _left = 1;\r\n    private int _right = 2;\r\n    private double _double = 0.0;\r\n    private int _bits = 4;\r\n\r\n    [Benchmark]\r\n    public bool ImplicitElse() =&gt; ImplicitElse(_left, _right);\r\n\r\n    [Benchmark]\r\n    public bool IsDefaultValue() =&gt; IsDefaultValue(_double);\r\n\r\n    [Benchmark]\r\n    public bool HasEitherBit() =&gt; HasEitherBit(_bits);\r\n\r\n    [MethodImpl(MethodImplOptions.NoInlining)]\r\n    private static bool ImplicitElse(int left, int right)\r\n    {\r\n        bool leftIsSmaller = false;\r\n        if (left &lt; right)\r\n            leftIsSmaller = true;\r\n\r\n        return leftIsSmaller;\r\n    }\r\n\r\n    [MethodImpl(MethodImplOptions.NoInlining)]\r\n    private static bool IsDefaultValue(double value) =&gt; 0.0.Equals(value);\r\n\r\n    [MethodImpl(MethodImplOptions.NoInlining)]\r\n    private static bool HasEitherBit(int value) =&gt;\r\n        ((value &amp; 4) == 4) || ((value &amp; 8) == 8);\r\n}<\/code><\/pre>\n<p>For <code>ImplicitElse<\/code>, .NET 10 already avoids a branch, but it still materializes both Boolean values and selects between them. In .NET 11, the method becomes just the comparison and a <code>cset<\/code>, shrinking from 36 bytes to 24 bytes:<\/p>\n<pre><code class=\"language-diff\">; Arm64\r\n--- .NET 10\r\n+++ .NET 11\r\n-            mov     w2, wzr\r\n-            mov     w3, #1\r\n             cmp     w0, w1\r\n-            csel    w2, w2, w3, ge\r\n-            mov     w0, w2\r\n+            cset    x0, lt\r\n\r\n-; Total bytes of code 36\r\n+; Total bytes of code 24<\/code><\/pre>\n<p><code>0.0.Equals(value)<\/code> needs to account for <code>NaN<\/code>, but because the left operand is zero, the case where both operands are <code>NaN<\/code> can never apply. Removing the conditional selection for that case leaves one floating-point comparison and one <code>cset<\/code>, reducing <code>IsDefaultValue<\/code> from 40 bytes to 24 bytes:<\/p>\n<pre><code class=\"language-diff\">; Arm64\r\n--- .NET 10\r\n+++ .NET 11\r\n             fcmp    d0, #0.0\r\n-            beq     G_M000_IG04\r\n-\r\n-G_M000_IG03:\r\n-            fcmp    d0, d0\r\n-            csel    w0, wzr, wzr, eq\r\n-            b       G_M000_IG05\r\n-\r\n-G_M000_IG04:\r\n-            mov     w0, #1\r\n-\r\n-G_M000_IG05:\r\n+            cset    x0, eq\r\n+\r\n+G_M000_IG03:\r\n             ldp     fp, lr, [sp], #0x10\r\n             ret     lr\r\n\r\n-; Total bytes of code 40\r\n+; Total bytes of code 24<\/code><\/pre>\n<p>Finally, normalizing both power-of-two comparisons lets the JIT combine their results. The short-circuit branch in <code>HasEitherBit<\/code> is replaced by two masks and an <code>or<\/code>, reducing the method from 40 bytes to 36 bytes:<\/p>\n<pre><code class=\"language-diff\">; Arm64\r\n--- .NET 10\r\n+++ .NET 11\r\n-            tbz     w0, #2, G_M000_IG05\r\n-\r\n-G_M000_IG03:\r\n-            mov     w0, #1\r\n-\r\n-G_M000_IG04:\r\n-            ldp     fp, lr, [sp], #0x10\r\n-            ret     lr\r\n-\r\n-G_M000_IG05:\r\n-            tst     w0, #8\r\n+            and     w1, w0, #4\r\n+            and     w0, w0, #8\r\n+            orr     w0, w1, w0\r\n+            cmp     w0, #0\r\n             cset    x0, ne\r\n\r\n-G_M000_IG06:\r\n+G_M000_IG03:\r\n             ldp     fp, lr, [sp], #0x10\r\n             ret     lr\r\n\r\n-; Total bytes of code 40\r\n+; Total bytes of code 36<\/code><\/pre>\n<p>Not every simplification depends on broader control-flow reasoning.\n&#8220;Peephole optimizations&#8221; instead replace a short, recognizable pattern with an\nequivalent cheaper one. Each may save only an instruction or expose a form\nthat another optimization understands, but these patterns can occur very\nfrequently on hot paths throughout generated code. For example, <a href=\"https:\/\/github.com\/dotnet\/runtime\/pull\/126529\">dotnet\/runtime#126529<\/a> from <a href=\"https:\/\/github.com\/BoyBaykiller\">@BoyBaykiller<\/a> recognizes that <code>255 - x<\/code> for a <code>byte<\/code> is equivalent to <code>x ^ 255<\/code>: both simply flip all eight bits, but the latter can remove an instruction if it&#8217;s able to replace a negation and add with an xor. Similarly, <code>-1 - x<\/code> can turn into the equivalent of <code>~x<\/code>.<\/p>\n<pre><code class=\"language-csharp\">\/\/ dotnet run -c Release -f net10.0 --filter \"*\" --runtimes net10.0 net11.0\r\n\r\nusing BenchmarkDotNet.Attributes;\r\nusing BenchmarkDotNet.Running;\r\n\r\nBenchmarkSwitcher.FromAssembly(typeof(Benchmarks).Assembly).Run(args);\r\n\r\n[DisassemblyDiagnoser, HideColumns(\"Job\", \"Error\", \"StdDev\", \"Median\", \"RatioSD\")]\r\npublic class Benchmarks\r\n{\r\n    private readonly byte[] _data = new byte[4096];\r\n\r\n    [GlobalSetup]\r\n    public void Setup() =&gt; new Random(42).NextBytes(_data);\r\n\r\n    [Benchmark]\r\n    public int InvertBytes()\r\n    {\r\n        int sum = 0;\r\n        foreach (byte b in _data) sum += 255 - b;\r\n        return sum;\r\n    }\r\n}<\/code><\/pre>\n<p>In .NET 11, the loop loses a separate negate and add:<\/p>\n<pre><code class=\"language-diff\">; Arm64\r\n--- .NET 10\r\n+++ .NET 11\r\n@@ -19,9 +19,8 @@\r\n\r\n G_M000_IG04:\r\n             ldrb    w4, [x0, w2, UXTW]\r\n-            neg     w4, w4\r\n+            eor     w4, w4, #255\r\n             add     w1, w4, w1\r\n-            add     w1, w1, #255\r\n             add     w2, w2, #1\r\n             cmp     w3, w2\r\n             bgt     G_M000_IG04\r\n@@ -33,4 +32,4 @@\r\n             ldp     fp, lr, [sp], #0x10\r\n             ret     lr\r\n\r\n-; Total bytes of code 76\r\n+; Total bytes of code 72<\/code><\/pre>\n<p><a href=\"https:\/\/github.com\/dotnet\/runtime\/pull\/129361\">dotnet\/runtime#129361<\/a> removes another unnecessary instruction when comparing an <code>sbyte<\/code> with a constant that fits in eight bits. The JIT can compare the byte directly, with no sign extension:<\/p>\n<pre><code class=\"language-csharp\">\/\/ dotnet run -c Release -f net10.0 --filter \"*\" --runtimes net10.0 net11.0\r\n\r\nusing BenchmarkDotNet.Attributes;\r\nusing BenchmarkDotNet.Running;\r\nusing System.Runtime.CompilerServices;\r\n\r\nBenchmarkSwitcher.FromAssembly(typeof(Benchmarks).Assembly).Run(args);\r\n\r\n[DisassemblyDiagnoser, HideColumns(\"Job\", \"Error\", \"StdDev\", \"Median\", \"RatioSD\")]\r\npublic class Benchmarks\r\n{\r\n    private sbyte _value = -65;\r\n\r\n    [Benchmark]\r\n    public bool IsLow() =&gt; IsLow(_value);\r\n\r\n    [MethodImpl(MethodImplOptions.NoInlining)]\r\n    private static bool IsLow(sbyte value) =&gt; value &lt; -64;\r\n}<\/code><\/pre>\n<p>The optimized codegen then compares the byte directly, removing the <code>movsx<\/code> sign-extension instruction (though the JIT still retains it in the few comparison forms that require a full-width sign bit for correctness).<\/p>\n<pre><code class=\"language-diff\">; x64\r\n--- .NET 10\r\n+++ .NET 11\r\n-movsx  rax,cl\r\n-cmp    eax,0FFFFFFC0\r\n+cmp    cl,0C0\r\n setl   al\r\n movzx  eax,al\r\n ret\r\n\r\n-; Total bytes of code 14\r\n+; Total bytes of code 10<\/code><\/pre>\n<p><a href=\"https:\/\/github.com\/dotnet\/runtime\/pull\/125180\">dotnet\/runtime#125180<\/a> from <a href=\"https:\/\/github.com\/saucecontrol\">@saucecontrol<\/a> improves non-overflowing <code>float<\/code> and <code>double<\/code> conversions to <code>long<\/code> and <code>ulong<\/code> on x86 machines with AVX-512 or AVX10.2. These casts have defined behavior for NaN and out-of-range values, so older code used a helper to preserve those semantics. The newer instruction set lets the JIT keep the normal path inline and register-based, avoiding the helper call; machines that don&#8217;t support these instructions retain the existing fallback.<\/p>\n<pre><code class=\"language-csharp\">\/\/ Run with 32-bit x86 dotnet on a machine with AVX-512 or AVX10.2:\r\n\/\/ dotnet run -c Release -f net10.0 --filter \"*\" --runtimes net10.0 net11.0\r\n\r\nusing BenchmarkDotNet.Attributes;\r\nusing BenchmarkDotNet.Running;\r\n\r\nBenchmarkSwitcher.FromAssembly(typeof(Benchmarks).Assembly).Run(args);\r\n\r\n[DisassemblyDiagnoser, HideColumns(\"Job\", \"Error\", \"StdDev\", \"Median\", \"RatioSD\")]\r\npublic class Benchmarks\r\n{\r\n    private float _single = 123_456.75f;\r\n    private double _double = 123_456.75;\r\n\r\n    [Benchmark] public long SingleToInt64() =&gt; (long)_single;\r\n    [Benchmark] public ulong SingleToUInt64() =&gt; (ulong)_single;\r\n    [Benchmark] public long DoubleToInt64() =&gt; (long)_double;\r\n    [Benchmark] public ulong DoubleToUInt64() =&gt; (ulong)_double;\r\n}<\/code><\/pre>\n<table>\n<thead>\n<tr>\n<th>Method<\/th>\n<th>Runtime<\/th>\n<th style=\"text-align: right;\">Mean<\/th>\n<th style=\"text-align: right;\">Ratio<\/th>\n<th style=\"text-align: right;\">Code Size<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td>SingleToInt64<\/td>\n<td>.NET 10.0<\/td>\n<td style=\"text-align: right;\">5.103 ns<\/td>\n<td style=\"text-align: right;\">1.00<\/td>\n<td style=\"text-align: right;\">31 B<\/td>\n<\/tr>\n<tr>\n<td>SingleToInt64<\/td>\n<td>.NET 11.0<\/td>\n<td style=\"text-align: right;\">2.093 ns<\/td>\n<td style=\"text-align: right;\">0.41<\/td>\n<td style=\"text-align: right;\">54 B<\/td>\n<\/tr>\n<tr>\n<td><\/td>\n<td><\/td>\n<td style=\"text-align: right;\"><\/td>\n<td style=\"text-align: right;\"><\/td>\n<td style=\"text-align: right;\"><\/td>\n<\/tr>\n<tr>\n<td>SingleToUInt64<\/td>\n<td>.NET 10.0<\/td>\n<td style=\"text-align: right;\">4.810 ns<\/td>\n<td style=\"text-align: right;\">1.00<\/td>\n<td style=\"text-align: right;\">31 B<\/td>\n<\/tr>\n<tr>\n<td>SingleToUInt64<\/td>\n<td>.NET 11.0<\/td>\n<td style=\"text-align: right;\">1.366 ns<\/td>\n<td style=\"text-align: right;\">0.28<\/td>\n<td style=\"text-align: right;\">34 B<\/td>\n<\/tr>\n<tr>\n<td><\/td>\n<td><\/td>\n<td style=\"text-align: right;\"><\/td>\n<td style=\"text-align: right;\"><\/td>\n<td style=\"text-align: right;\"><\/td>\n<\/tr>\n<tr>\n<td>DoubleToInt64<\/td>\n<td>.NET 10.0<\/td>\n<td style=\"text-align: right;\">4.834 ns<\/td>\n<td style=\"text-align: right;\">1.00<\/td>\n<td style=\"text-align: right;\">31 B<\/td>\n<\/tr>\n<tr>\n<td>DoubleToInt64<\/td>\n<td>.NET 11.0<\/td>\n<td style=\"text-align: right;\">2.101 ns<\/td>\n<td style=\"text-align: right;\">0.43<\/td>\n<td style=\"text-align: right;\">54 B<\/td>\n<\/tr>\n<tr>\n<td><\/td>\n<td><\/td>\n<td style=\"text-align: right;\"><\/td>\n<td style=\"text-align: right;\"><\/td>\n<td style=\"text-align: right;\"><\/td>\n<\/tr>\n<tr>\n<td>DoubleToUInt64<\/td>\n<td>.NET 10.0<\/td>\n<td style=\"text-align: right;\">4.663 ns<\/td>\n<td style=\"text-align: right;\">1.00<\/td>\n<td style=\"text-align: right;\">31 B<\/td>\n<\/tr>\n<tr>\n<td>DoubleToUInt64<\/td>\n<td>.NET 11.0<\/td>\n<td style=\"text-align: right;\">1.363 ns<\/td>\n<td style=\"text-align: right;\">0.29<\/td>\n<td style=\"text-align: right;\">34 B<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<h3>Vectorization<\/h3>\n<p>SIMD, or &#8220;single instruction, multiple data&#8221;, is the concept of one instruction applying the same operation to several values at once. A &#8220;scalar&#8221; <code>add<\/code>, for example, might combine one pair of 32-bit integers, while a 128-bit SIMD <code>add<\/code> can combine &#8220;vectors&#8221; of four pairs in the same instruction; 256- and 512-bit variants can handle vectors of eight and sixteen pairs, respectively. When the iterations of an operation are independent, &#8220;vectorizing&#8221; a loop can therefore replace several scalar iterations with one, improving the throughput of the loop significantly.<\/p>\n<p>.NET exposes portable (they work on any machine) variable-width vector type <code>Vector&lt;T&gt;<\/code> (which can represent different counts of <code>T<\/code> depending on the current hardware), fixed-width <code>Vector64&lt;T&gt;<\/code> through <code>Vector512&lt;T&gt;<\/code> types (which always represent the same count of <code>T<\/code>), and architecture-specific intrinsics (performing operations on such vector types which the JIT then maps to the right underlying hardware instructions). Each element in a vector is often referred to as a &#8220;lane&#8221;. Because the JIT recognizes these operations directly, it can fold constants, select instructions, and remove unsupported paths without treating them as normal method calls.<\/p>\n<p>A variety of PRs in .NET 11 improve AVX-512 broadcasting and masking. Embedded broadcasting lets an instruction load a single scalar value and replicate it across all vector lanes, avoiding the need to materialize a full-width vector constant in memory to feed into the instruction. For example, this bitwise AND instruction:<\/p>\n<pre><code class=\"language-asm\">; x64\r\nvpandd  zmm0, zmm1, dword ptr [reloc @RWD00] {1to16}<\/code><\/pre>\n<p>can replace this one:<\/p>\n<pre><code class=\"language-asm\">; x64\r\nvpandd  zmm0, zmm1, zmmword ptr [reloc @RWD00]<\/code><\/pre>\n<p>storing only 4 bytes in the read-only data section rather than 64. Because the broadcast is handled as part of the load, there&#8217;s no additional instruction-level latency; the primary benefit is reduced data size and cache footprint.<\/p>\n<p>Embedded masking similarly lets an instruction update only a subset of the lanes. A mask is one bit per vector lane, where each bit indicates whether and how the operation should affect the corresponding lane. Without embedded masking, code often needs to compute every lane and then blend that result with the old value, so folding the mask into the operation can remove both the separate blend and a zero-vector setup. <a href=\"https:\/\/github.com\/dotnet\/runtime\/pull\/117700\">dotnet\/runtime#117700<\/a> from <a href=\"https:\/\/github.com\/saucecontrol\">@saucecontrol<\/a> improves broadcast selection when an intrinsic&#8217;s natural element size differs from its managed vector type. VNNI, the Vector Neural Network Instructions used for small-integer multiply-accumulate operations, and bitwise operations can now use the smallest valid repeated constant, avoiding a full-vector load.<\/p>\n<pre><code class=\"language-csharp\">\/\/ dotnet run -c Release -f net10.0 --filter \"*\" --runtimes net10.0 net11.0\r\n\/\/ Requires AVX-VNNI and AVX-512F.\r\n\r\nusing BenchmarkDotNet.Attributes;\r\nusing BenchmarkDotNet.Running;\r\nusing System.Runtime.Intrinsics;\r\nusing System.Runtime.Intrinsics.X86;\r\n\r\nBenchmarkSwitcher.FromAssembly(typeof(Benchmarks).Assembly).Run(args);\r\n\r\n[DisassemblyDiagnoser, HideColumns(\"Job\", \"Error\", \"StdDev\", \"Median\", \"RatioSD\")]\r\npublic class Benchmarks\r\n{\r\n    private readonly Vector128&lt;byte&gt; _bytes = Vector128.Create((byte)1);\r\n    private readonly Vector128&lt;ulong&gt; _u64 = Vector128.Create(1UL);\r\n    private readonly Vector128&lt;uint&gt; _u32 = Vector128.Create(1U);\r\n    private readonly Vector512&lt;int&gt; _v512 = Vector512.Create(1);\r\n    private int _n = 42;\r\n\r\n    [Benchmark]\r\n    public Vector128&lt;int&gt; VnniBroadcast() =&gt;\r\n        AvxVnni.MultiplyWideningAndAdd(\r\n            Vector128&lt;int&gt;.Zero, _bytes, Vector128&lt;sbyte&gt;.One);\r\n\r\n    [Benchmark]\r\n    public Vector128&lt;uint&gt; MaskAnd() =&gt;\r\n        Vector128.ConditionalSelect(\r\n            Vector128.GreaterThan(_u32, Vector128&lt;uint&gt;.Zero),\r\n            (_u64 &amp; Vector128&lt;uint&gt;.One.AsUInt64()).AsUInt32(),\r\n            Vector128&lt;uint&gt;.Zero);\r\n\r\n    [Benchmark]\r\n    public Vector512&lt;int&gt; BlendMaskAllOnes() =&gt;\r\n        Avx512F.BlendVariable(\r\n            Vector512.Create(_n),\r\n            _v512,\r\n            Vector512.Create(-1));\r\n\r\n    [Benchmark]\r\n    public Vector512&lt;int&gt; MultiInsert() =&gt;\r\n        Vector512.ConditionalSelect(\r\n            Vector512.Create(0, -1, 0, 0, 0, -1, 0, 0, 0, -1, 0, 0, 0, -1, 0, 0),\r\n            _v512,\r\n            Vector512.Create(_n));\r\n\r\n    [Benchmark]\r\n    public Vector512&lt;int&gt; MultiInsertZero() =&gt;\r\n        Avx512F.BlendVariable(\r\n            _v512,\r\n            Vector512&lt;int&gt;.Zero,\r\n            Vector512.Create(0, -1, 0, 0, 0, -1, 0, 0, 0, -1, 0, 0, 0, -1, 0, 0));\r\n}<\/code><\/pre>\n<p>In .NET 11, this results in 12 fewer bytes in the read-only data section, and\n12 fewer bytes of constant-pool cache footprint.<\/p>\n<pre><code class=\"language-diff\">; x64\r\n-C4E279503500000000   vpdpbusd xmm6, xmm0, xmmword ptr [reloc @RWD00]\r\n+62F27D18503500000000 vpdpbusd xmm6, xmm0, dword ptr [reloc @RWD00] {1to4}\r\n\r\n-RWD00  dq 0101010101010101h, 0101010101010101h\r\n+RWD00  dd 01010101h<\/code><\/pre>\n<p>The fix also impacts embedded masking. For example, with <code>MaskAnd<\/code> previously, the AND used a qword broadcast, <code>{1to2}<\/code>, and a separate blend then moved the masked result, meaning two instructions. Now that <code>Vector128&lt;uint&gt;.One<\/code> can be broadcast at dword granularity, the mask&#8217;s element size and the AND&#8217;s element size agree, unlocking using the single merged-masked form. This pattern shows up throughout vectorized algorithms that do lots of bitwise manipulation and hashing, including implementations in System.Numerics.Tensors, System.IO.Hashing, and System.Private.CoreLib.<\/p>\n<pre><code class=\"language-diff\">; x64\r\n-       vpandq   xmm0, xmm0, qword ptr [reloc @RWD00] {1to2}\r\n-       vpblendmd xmm0 {k1}{z}, xmm0, xmm0\r\n+       vpandd   xmm0 {k1}{z}, xmm0, dword ptr [reloc @RWD00] {1to4}\r\n\r\n; Code: 45 \u2192 39 bytes; data: 8 bytes \u2192 4 bytes<\/code><\/pre>\n<p>That <code>MaskAnd<\/code> example starts as an AND followed by a blend, an operation that\nchooses independently for each vector lane whether to take its value from one\ninput or the other, with the JIT able to fold those two operations together.\nSimilar opportunities arise with blends more generally. Sometimes the mask or\none of the inputs makes the choice trivial, e.g. an all-ones mask always selects the\nsame input, so the blend is just a move. If one input is zero, it can often\nbecome an <code>AND<\/code> or <code>ANDN<\/code>. AVX-512 provides more options still, as constant\nmasks and zeroing can be encoded directly in the instruction.\n<a href=\"https:\/\/github.com\/dotnet\/runtime\/pull\/123146\">dotnet\/runtime#123146<\/a> from\n<a href=\"https:\/\/github.com\/saucecontrol\">@saucecontrol<\/a> makes these simplifications\nconsistently across the portable and hardware-specific APIs. A blend with an all-ones mask provides a particularly clear example:<\/p>\n<pre><code class=\"language-csharp\">\/\/ Run on x64 with AVX-512:\r\n\/\/ dotnet run -c Release -f net10.0 --filter \"*\" --runtimes net10.0 net11.0\r\n\r\nusing System.Runtime.Intrinsics;\r\nusing System.Runtime.Intrinsics.X86;\r\nusing BenchmarkDotNet.Attributes;\r\nusing BenchmarkDotNet.Running;\r\n\r\nBenchmarkSwitcher.FromAssembly(typeof(Benchmarks).Assembly).Run(args);\r\n\r\n[DisassemblyDiagnoser]\r\n[HideColumns(\"Job\", \"Error\", \"StdDev\", \"Median\", \"RatioSD\")]\r\npublic class Benchmarks\r\n{\r\n    private readonly Vector512&lt;int&gt; _values = Vector512.Create(1);\r\n    private int _n = 42;\r\n\r\n    [GlobalSetup]\r\n    public void Setup()\r\n    {\r\n        if (!Avx512F.IsSupported)\r\n            throw new PlatformNotSupportedException();\r\n    }\r\n\r\n    [Benchmark]\r\n    public Vector512&lt;int&gt; BlendMaskAllOnes() =&gt;\r\n        Avx512F.BlendVariable(Vector512.Create(_n), _values, Vector512.Create(-1));\r\n}<\/code><\/pre>\n<p>The generated code no longer needs\nto create the first input, load the mask, or perform the blend. It simply\nloads the input the all-ones mask would always select:<\/p>\n<pre><code class=\"language-diff\">; x64\r\n-vpbroadcastd zmm0, dword ptr [rcx+8]\r\n-kmovq       k1, qword ptr [RWD00]\r\n-vpblendmd   zmm0 {k1}, zmm0, [rcx+48]\r\n+vmovups     zmm0, [rcx+48]\r\n vmovups     [rdx], zmm0\r\n mov         rax, rdx\r\n vzeroupper\r\n ret\r\n\r\n; 39 bytes \u2192 23 bytes<\/code><\/pre>\n<h3>Intrinsics<\/h3>\n<p>An intrinsic is a managed API that the JIT recognizes and special-cases. Often that special-casing involves actually replacing calls to the method with custom code that&#8217;s behaviorally equivalent but better in some way (faster, smaller, etc.)<\/p>\n<p>As an example, <a href=\"https:\/\/github.com\/dotnet\/runtime\/pull\/128678\">dotnet\/runtime#128678<\/a> improves recognition of generic-math calls to <code>IBinaryNumber&lt;T&gt;.Log2<\/code>. The method computes the base-2 logarithm of an integer, equivalent to the index of the number&#8217;s highest set bit; for example, <code>Log2(16)<\/code> is <code>4<\/code>. Previously, the JIT&#8217;s normalized integer type lost the signedness needed to import the operation directly as an intrinsic. Inlining the managed implementation could still produce the same optimized code, but when inlining didn&#8217;t happen, the managed call remained. In .NET 11, the JIT consults the precise type and imports the operation directly: unsigned and non-negative signed inputs can become leading-zero-count or bit-scan arithmetic, while a negative signed value retains the managed fallback and its exact exception behavior.<\/p>\n<p>Sometimes the JIT has a perfectly good intrinsic lowering but doesn&#8217;t recognize a call that should use it. <code>Enum.Equals<\/code> from a generic <code>T : Enum<\/code> context was a good example. Even though both arguments to the generic helper are strongly typed as <code>T<\/code>, an enum doesn&#8217;t provide an <code>Equals(T)<\/code> method; it inherits the virtual <code>Enum.Equals(object)<\/code> implementation. The second argument therefore needs to be boxed to pass it as <code>object<\/code>. The receiver is invoked with a constrained virtual call, but because the concrete enum doesn&#8217;t override the method itself, it too needs to be boxed to invoke the implementation on <code>System.Enum<\/code>. Thus, what looks like a strongly-typed comparison can end up allocating two boxes and making a virtual call. In .NET 11, <a href=\"https:\/\/github.com\/dotnet\/runtime\/pull\/122779\">dotnet\/runtime#122779<\/a> eliminates this overhead by teaching the JIT to recognize the call and fold it to a direct comparison of the enum&#8217;s underlying integer values. For example:<\/p>\n<pre><code class=\"language-csharp\">\/\/ dotnet run -c Release -f net10.0 --filter \"*\" --runtimes net10.0 net11.0\r\n\r\nusing BenchmarkDotNet.Attributes;\r\nusing BenchmarkDotNet.Running;\r\nusing System.Runtime.CompilerServices;\r\n\r\nBenchmarkSwitcher.FromAssembly(typeof(Benchmarks).Assembly).Run(args);\r\n\r\n[MemoryDiagnoser(false), HideColumns(\"Job\", \"Error\", \"StdDev\", \"Median\", \"RatioSD\")]\r\npublic class Benchmarks\r\n{\r\n    private static readonly StringComparison[] s_values =\r\n    {\r\n        StringComparison.Ordinal, StringComparison.OrdinalIgnoreCase, \r\n        StringComparison.CurrentCulture, StringComparison.CurrentCultureIgnoreCase,\r\n        StringComparison.InvariantCulture, StringComparison.Ordinal,\r\n    };\r\n\r\n    [Benchmark]\r\n    public int CountOrdinal_Generic()\r\n    {\r\n        int count = 0;\r\n        foreach (var v in s_values)\r\n            if (EqualsGeneric(v, StringComparison.Ordinal))\r\n                count++;\r\n\r\n        return count;\r\n    }\r\n\r\n    [MethodImpl(MethodImplOptions.NoInlining)]\r\n    private static bool EqualsGeneric&lt;T&gt;(T a, T b) where T : Enum =&gt; a.Equals(b);\r\n}<\/code><\/pre>\n<p>Once the JIT knows the callee is <code>Enum.Equals<\/code> and knows the exact enum type, it asks the runtime for the underlying integer type and replaces the virtual call with a direct comparison. That in turn makes both box\/unbox pairs redundant, and the generated code contains neither allocation. For the six comparisons performed here, .NET 10 creates twelve boxes, totaling 288 bytes. In .NET 11, the helper becomes just the integer comparison, eliminating both the allocations and the virtual dispatch.<\/p>\n<table>\n<thead>\n<tr>\n<th>Method<\/th>\n<th>Runtime<\/th>\n<th>Mean<\/th>\n<th>Ratio<\/th>\n<th>Allocated<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td>CountOrdinal_Generic<\/td>\n<td>.NET 10.0<\/td>\n<td>59.24 ns<\/td>\n<td>1.00<\/td>\n<td>288 B<\/td>\n<\/tr>\n<tr>\n<td>CountOrdinal_Generic<\/td>\n<td>.NET 11.0<\/td>\n<td>10.01 ns<\/td>\n<td>0.17<\/td>\n<td>&#8211;<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<p>NativeAOT had been carrying an equivalent optimization for years, implemented as IL rewriting in ILCompiler that patches <code>Enum.Equals<\/code> to use typed comparisons. With the JIT now handling it, including in NativeAOT&#8217;s own use of the JIT (NativeAOT uses the JIT ahead of time rather than just in time), <a href=\"https:\/\/github.com\/dotnet\/runtime\/pull\/123086\">dotnet\/runtime#123086<\/a> deletes that rewriting and its supporting machinery.<\/p>\n<p><a href=\"https:\/\/github.com\/dotnet\/runtime\/pull\/127329\">dotnet\/runtime#127329<\/a> improves the <code>Vector256.Sum<\/code> and <code>Vector512.Sum<\/code> intrinsics. The JIT now performs most of the reduction at full width and combines the per-lane results at the end, avoiding the extracts and duplicate shuffle sequences needed when splitting wide vectors into 128-bit pieces. And <a href=\"https:\/\/github.com\/dotnet\/runtime\/pull\/127402\">dotnet\/runtime#127402<\/a> extends vector-constant propagation from 128-bit vectors to <code>Vector256<\/code> and <code>Vector512<\/code>. Code that compares a wide vector with a known sentinel can now simplify subsequent uses just as narrower vectors already could. The following benchmark exemplifies both:<\/p>\n<pre><code class=\"language-csharp\">\/\/ dotnet run -c Release -f net10.0 --filter \"*\" --runtimes net10.0 net11.0\r\n\/\/ Requires AVX2 for the assembly shown below.\r\n\r\nusing BenchmarkDotNet.Attributes;\r\nusing BenchmarkDotNet.Running;\r\nusing System.Runtime.CompilerServices;\r\nusing System.Runtime.Intrinsics;\r\n\r\nBenchmarkSwitcher.FromAssembly(typeof(Benchmarks).Assembly).Run(args);\r\n\r\n[DisassemblyDiagnoser, HideColumns(\"Job\", \"Error\", \"StdDev\", \"Median\", \"RatioSD\")]\r\npublic class Benchmarks\r\n{\r\n    private readonly Vector256&lt;float&gt; _floats =\r\n        Vector256.Create(1.0f, 2.0f, 3.0f, 4.0f, 5.0f, 6.0f, 7.0f, 8.0f);\r\n    private int _selector;\r\n\r\n    [Benchmark]\r\n    public float Sum() =&gt; Vector256.Sum(_floats);\r\n\r\n    [Benchmark]\r\n    public int TransformWhenKnown()\r\n    {\r\n        Vector256&lt;int&gt; value = GetVector();\r\n        if (value == Vector256.Create(0, 1, 2, 3, 4, 5, 6, 7))\r\n            return (value + Vector256.Create(10)).GetElement(6);\r\n\r\n        return -1;\r\n    }\r\n\r\n    [MethodImpl(MethodImplOptions.NoInlining)]\r\n    private Vector256&lt;int&gt; GetVector() =&gt;\r\n        _selector == 0 ?\r\n            Vector256.Create(0, 1, 2, 3, 4, 5, 6, 7) :\r\n            Vector256.Create(7);\r\n}<\/code><\/pre>\n<p>For <code>Sum<\/code>, .NET 10 separately reduces each 128-bit half and then adds the two scalar results. In .NET 11, the permutes and adds operate on both halves in parallel as 256-bit instructions, after which only the two already-reduced halves need to be combined:<\/p>\n<pre><code class=\"language-diff\">; x64\r\n vmovups   ymm0, [rcx+28]\r\n-vmovaps   ymm1, ymm0\r\n-vpermilps xmm2, xmm1, 0B1\r\n-vaddps    xmm1, xmm2, xmm1\r\n-vpermilps xmm2, xmm1, 4E\r\n-vaddps    xmm1, xmm2, xmm1\r\n-vextractf128 xmm0, ymm0, 1\r\n-vpermilps xmm2, xmm0, 0B1\r\n-vaddps    xmm0, xmm2, xmm0\r\n-vpermilps xmm2, xmm0, 4E\r\n-vaddps    xmm0, xmm2, xmm0\r\n-vaddss    xmm0, xmm1, xmm0\r\n+vpermilps ymm1, ymm0, 0B1\r\n+vaddps    ymm0, ymm1, ymm0\r\n+vpermilps ymm1, ymm0, 4E\r\n+vaddps    ymm0, ymm1, ymm0\r\n+vextractf128 xmm1, ymm0, 1\r\n+vaddps    xmm0, xmm1, xmm0\r\n\r\n; 63 bytes \u2192 39 bytes<\/code><\/pre>\n<p><code>TransformWhenKnown<\/code> uses a deliberately non-repeating constant across its eight lanes. On the branch where the comparison succeeds, .NET 11 can replace <code>value<\/code> with that constant, fold the vector addition, and determine that element 6 is <code>16<\/code>. The <code>vpaddd<\/code>, extraction, second 32-byte constant, and associated control flow all disappear:<\/p>\n<pre><code class=\"language-diff\">; x64\r\n-cmp      eax, 0FFFFFFFF\r\n-jne      M00_L00\r\n-vmovups  ymm0, [rsp+20]\r\n-vpaddd   ymm0, ymm0, [RWD32]\r\n-vextracti128 xmm0, ymm0, 1\r\n-vpextrd  eax, xmm0, 2\r\n-vzeroupper\r\n-add      rsp, 58\r\n-ret\r\n-\r\n-M00_L00:\r\n-mov      eax, 0FFFFFFFF\r\n+mov      ecx, 0FFFFFFFF\r\n+mov      edx, 10\r\n+cmp      eax, 0FFFFFFFF\r\n+mov      eax, edx\r\n+cmovne   eax, ecx\r\n vzeroupper\r\n add      rsp, 58\r\n ret\r\n\r\n; 85 bytes \u2192 59 bytes<\/code><\/pre>\n<p>One of the goals of .NET is that you can write code once and have it run anywhere, optimized for whatever that &#8220;anywhere&#8221; has to offer. For vectorization, that means providing portable operations whenever the intent is common across instruction sets, while retaining architecture-specific APIs for algorithms that really do need to target a particular machine.<\/p>\n<p>Whenever possible, we want to enable developers to express their algorithms using the portable APIs, and each release of .NET fills additional gaps there. Including .NET 11. <a href=\"https:\/\/github.com\/dotnet\/runtime\/pull\/129627\">dotnet\/runtime#129627<\/a> from <a href=\"https:\/\/github.com\/hez2010\">@hez2010<\/a> adds portable APIs for constructing common lane sequences (e.g. <code>[1, 2, 4, 8]<\/code> or <code>[a, b, a, b]<\/code>), concatenating half-vectors (the lower halves of <code>[a, b, c, d]<\/code> and <code>[w, x, y, z]<\/code> producing <code>[a, b, w, x]<\/code>), interleaving (<code>[a, b]<\/code> and <code>[x, y]<\/code> producing <code>[a, x, b, y]<\/code>), de-interleaving (<code>[a, x, b, y]<\/code> producing <code>[a, b]<\/code> and <code>[x, y]<\/code>), and reversal (<code>[a, b, c, d]<\/code> producing <code>[d, c, b, a]<\/code>), along with their JIT intrinsification. These operations were already expressible, but only verbosely and only if you knew which hardware instruction to reach for, e.g. writing <code>Zip<\/code> by hand meant targeting a platform-specific API like <code>AdvSimd.Arm64.ZipLow<\/code>. The new APIs let the code state the transformation and leave instruction selection to the JIT.<\/p>\n<p>Once the intrinsic operation has been recognized, the backend still needs to keep it in a useful vector form while assigning registers and selecting instructions. Vector values are structs, and the JIT will often apply &#8220;struct promotion,&#8221; tracking a struct&#8217;s fields as independent locals so that each can be optimized separately. That&#8217;s useful for ordinary structs, but counterproductive when a value is meant to remain in a vector or mask register: splitting it can introduce extra moves and obscure what should be a single whole-value store, particularly after inlining introduces more local stores. <a href=\"https:\/\/github.com\/dotnet\/runtime\/pull\/128013\">dotnet\/runtime#128013<\/a> consistently marks SIMD and mask stores as intrinsic-related across platforms, including 32-bit x86 and x64 mask stores, so those locals remain intact. <a href=\"https:\/\/github.com\/dotnet\/runtime\/pull\/129563\">dotnet\/runtime#129563<\/a> extends that principle to user-defined structs that are bitcast to SIMD types. This trades away struct promotion for those locals, but enables the JIT to preserve their vector representation.<\/p>\n<p>This matters for user-defined numerical types that store the same data as a\nhardware vector but expose named fields or domain-specific operations. The\nfollowing <code>Vector2Double<\/code> is laid out as two adjacent <code>double<\/code> values, so it can\nbe bitcast to <code>Vector128&lt;double&gt;<\/code>, operated on with SIMD, and bitcast back:<\/p>\n<pre><code class=\"language-csharp\">\/\/ dotnet run -c Release -f net10.0 --filter \"*\" --runtimes net10.0 net11.0\r\n\r\nusing System.Runtime.CompilerServices;\r\nusing System.Runtime.InteropServices;\r\nusing System.Runtime.Intrinsics;\r\nusing BenchmarkDotNet.Attributes;\r\nusing BenchmarkDotNet.Running;\r\n\r\nBenchmarkSwitcher.FromAssembly(typeof(Benchmarks).Assembly).Run(args);\r\n\r\npublic struct Vector2Double(double x, double y)\r\n{\r\n    public double X = x;\r\n    public double Y = y;\r\n\r\n    public static Vector2Double operator +(Vector2Double left, Vector2Double right)\r\n    {\r\n        Vector128&lt;double&gt; simdLeft = Unsafe.BitCast&lt;Vector2Double, Vector128&lt;double&gt;&gt;(left);\r\n        Vector128&lt;double&gt; simdRight = Unsafe.BitCast&lt;Vector2Double, Vector128&lt;double&gt;&gt;(right);\r\n        return Unsafe.BitCast&lt;Vector128&lt;double&gt;, Vector2Double&gt;(simdLeft + simdRight);\r\n    }\r\n}\r\n\r\n[DisassemblyDiagnoser, HideColumns(\"Job\", \"Error\", \"StdDev\", \"Median\", \"RatioSD\")]\r\npublic class Benchmarks\r\n{\r\n    private readonly Vector2Double _a = new(1.0, 2.0);\r\n    private readonly Vector2Double _b = new(3.0, 4.0);\r\n    private readonly Vector2Double _c = new(5.0, 6.0);\r\n\r\n    [Benchmark]\r\n    public Vector2Double Add() =&gt; Add(_a, _b, _c);\r\n\r\n    [MethodImpl(MethodImplOptions.NoInlining)]\r\n    private static Vector2Double Add(Vector2Double a, Vector2Double b, Vector2Double c) =&gt;\r\n        a + b + c;\r\n}<\/code><\/pre>\n<p>In .NET 10, promotion of the intermediate struct sends the first SIMD result\nthrough two stack locations before the second addition. .NET 11 keeps that\nvalue in <code>xmm0<\/code>, reducing the helper from 52 bytes to 22 bytes:<\/p>\n<pre><code class=\"language-diff\">; x64\r\n-sub       rsp, 28\r\n vmovups   xmm0, [rdx]\r\n vaddpd    xmm0, xmm0, [r8]\r\n-vmovaps   [rsp], xmm0\r\n-vmovups   xmm0, [rsp]\r\n-vmovups   [rsp+18], xmm0\r\n-vmovups   xmm0, [rsp+18]\r\n vaddpd    xmm0, xmm0, [r9]\r\n vmovups   [rcx], xmm0\r\n mov       rax, rcx\r\n-add       rsp, 28\r\n ret\r\n\r\n; 52 bytes \u2192 22 bytes<\/code><\/pre>\n<p><a href=\"https:\/\/github.com\/dotnet\/runtime\/pull\/128350\">dotnet\/runtime#128350<\/a> gives the xarch register allocator more freedom around fused multiply-add (FMA) and AVX-512 ternary-logic operations. These instructions can read and overwrite operands in several equivalent arrangements; choosing the arrangement that already matches the surrounding registers avoids otherwise necessary moves.<\/p>\n<p>Generic vector code introduces another wrinkle. Operators like\n<code>Vector128&lt;T&gt;.operator ==<\/code> return <code>bool<\/code>, so the return type doesn&#8217;t reveal the\nvector&#8217;s element type. The JIT instead needs to obtain that type from the\noperands in order to select the right comparison instruction. In some generic\ncontexts, including helpers built on the internal <code>ISimdVector<\/code> abstraction,\nthe JIT was consulting the wrong type information and failed to import the\noperator as an intrinsic. It then executed the managed fallback, which compares\nthe lanes individually. <a href=\"https:\/\/github.com\/dotnet\/runtime\/pull\/130086\">dotnet\/runtime#130086<\/a> marks these operators so their element type is taken from the first argument. As an example, the generic helpers used internally by ordinal-ignore-case string comparer benefit from this.<\/p>\n<pre><code class=\"language-csharp\">\/\/ dotnet run -c Release -f net10.0 --filter \"*\" --runtimes net10.0 net11.0\r\n\r\nusing BenchmarkDotNet.Attributes;\r\nusing BenchmarkDotNet.Running;\r\n\r\nBenchmarkSwitcher.FromAssembly(typeof(Benchmarks).Assembly).Run(args);\r\n\r\n[HideColumns(\"Job\", \"Error\", \"StdDev\", \"Median\", \"RatioSD\")]\r\npublic class Benchmarks\r\n{\r\n    private readonly string _lower = new('a', 256);\r\n    private readonly string _upper = new('A', 256);\r\n\r\n    [Benchmark]\r\n    public bool OrdinalIgnoreCase() =&gt; string.Equals(_lower, _upper, StringComparison.OrdinalIgnoreCase);\r\n}<\/code><\/pre>\n<table>\n<thead>\n<tr>\n<th>Method<\/th>\n<th>Runtime<\/th>\n<th style=\"text-align: right;\">Mean<\/th>\n<th style=\"text-align: right;\">Ratio<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td>OrdinalIgnoreCase<\/td>\n<td>.NET 10.0<\/td>\n<td style=\"text-align: right;\">27.794 ns<\/td>\n<td style=\"text-align: right;\">1.00<\/td>\n<\/tr>\n<tr>\n<td>OrdinalIgnoreCase<\/td>\n<td>.NET 11.0<\/td>\n<td style=\"text-align: right;\">21.861 ns<\/td>\n<td style=\"text-align: right;\">0.79<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<p>.NET 11 adds support for newer x86 capabilities while also\nimproving code generated for existing hardware. These changes benefit both\ndirect users of hardware intrinsics and portable vector code selected by the\nJIT. For example, <a href=\"https:\/\/github.com\/dotnet\/runtime\/pull\/124114\">dotnet\/runtime#124114<\/a> from <a href=\"https:\/\/github.com\/saucecontrol\">@saucecontrol<\/a> improves 32-bit x86 without AVX-512, where converting <code>uint<\/code> to <code>float<\/code> or <code>double<\/code> previously required a runtime helper. Older x86 conversion instructions accept signed integers, and half of the <code>uint<\/code> range doesn&#8217;t fit in a signed 32-bit value, which is why the helper existed. The JIT now emits an inline vector-instruction sequence that handles the high bit explicitly, avoiding the call and its register and stack overhead.<\/p>\n<pre><code class=\"language-csharp\">\/\/ Run with 32-bit x86 dotnet and AVX-512 disabled (DOTNET_EnableAVX512=0)\r\n\/\/ dotnet run -c Release -f net10.0 --filter \"*\" --runtimes net10.0 net11.0\r\n\r\nusing BenchmarkDotNet.Attributes;\r\nusing BenchmarkDotNet.Running;\r\n\r\nBenchmarkSwitcher.FromAssembly(typeof(Benchmarks).Assembly).Run(args);\r\n\r\n[DisassemblyDiagnoser, HideColumns(\"Job\", \"Error\", \"StdDev\", \"Median\", \"RatioSD\")]\r\npublic class Benchmarks\r\n{\r\n    private uint _value = 0xF123_4567;\r\n\r\n    [Benchmark] public float UInt32ToSingle() =&gt; _value;\r\n    [Benchmark] public double UInt32ToDouble() =&gt; _value;\r\n}<\/code><\/pre>\n<table>\n<thead>\n<tr>\n<th>Method<\/th>\n<th>Runtime<\/th>\n<th style=\"text-align: right;\">Mean<\/th>\n<th style=\"text-align: right;\">Ratio<\/th>\n<th style=\"text-align: right;\">Code Size<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td>UInt32ToSingle<\/td>\n<td>.NET 10.0<\/td>\n<td style=\"text-align: right;\">4.752 ns<\/td>\n<td style=\"text-align: right;\">1.00<\/td>\n<td style=\"text-align: right;\">37 B<\/td>\n<\/tr>\n<tr>\n<td>UInt32ToSingle<\/td>\n<td>.NET 11.0<\/td>\n<td style=\"text-align: right;\">2.403 ns<\/td>\n<td style=\"text-align: right;\">0.51<\/td>\n<td style=\"text-align: right;\">43 B<\/td>\n<\/tr>\n<tr>\n<td><\/td>\n<td><\/td>\n<td style=\"text-align: right;\"><\/td>\n<td style=\"text-align: right;\"><\/td>\n<td style=\"text-align: right;\"><\/td>\n<\/tr>\n<tr>\n<td>UInt32ToDouble<\/td>\n<td>.NET 10.0<\/td>\n<td style=\"text-align: right;\">4.727 ns<\/td>\n<td style=\"text-align: right;\">1.00<\/td>\n<td style=\"text-align: right;\">39 B<\/td>\n<\/tr>\n<tr>\n<td>UInt32ToDouble<\/td>\n<td>.NET 11.0<\/td>\n<td style=\"text-align: right;\">2.402 ns<\/td>\n<td style=\"text-align: right;\">0.51<\/td>\n<td style=\"text-align: right;\">41 B<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<p><a href=\"https:\/\/github.com\/dotnet\/runtime\/pull\/124804\">dotnet\/runtime#124804<\/a> from <a href=\"https:\/\/github.com\/alexcovington\">@alexcovington<\/a> adds the AVX-512 Bit Matrix Multiply APIs. A binary matrix treats each bit as an element and combines rows and columns with bitwise operations, not integer multiplication. The instructions are useful in areas such as error correction and CRC computation. Each replaces a much longer sequence of shifts, masks, and exclusive-ORs. And <a href=\"https:\/\/github.com\/dotnet\/runtime\/pull\/128365\">dotnet\/runtime#128365<\/a> from <a href=\"https:\/\/github.com\/jamesburton\">@jamesburton<\/a> adds <code>AvxVnni.V512<\/code>, extending the AVX-VNNI APIs from 256-bit to 512-bit operands so the small-integer dot products used by quantized machine-learning models can process 64 bytes per operation instead of 32.<\/p>\n<p><a href=\"https:\/\/github.com\/dotnet\/runtime\/pull\/126062\">dotnet\/runtime#126062<\/a> from <a href=\"https:\/\/github.com\/saucecontrol\">@saucecontrol<\/a> also avoids converting a vector selector into an AVX-512 mask register when the eventual operation still needs the vector form. In such cases, the older-looking vector blend is actually shorter and uses fewer resources:<\/p>\n<pre><code class=\"language-csharp\">\/\/ dotnet run -c Release -f net10.0 --filter \"*\" --runtimes net10.0 net11.0\r\n\r\nusing BenchmarkDotNet.Attributes;\r\nusing BenchmarkDotNet.Running;\r\nusing System.Runtime.Intrinsics;\r\nusing System.Runtime.Intrinsics.X86;\r\n\r\nBenchmarkSwitcher.FromAssembly(typeof(Benchmarks).Assembly).Run(args);\r\n\r\n[DisassemblyDiagnoser, HideColumns(\"Job\", \"Error\", \"StdDev\", \"Median\", \"RatioSD\")]\r\npublic class Benchmarks\r\n{\r\n    private readonly Vector128&lt;float&gt; _v1 = Vector128.Create(-1.0f, 2.0f, -3.0f, 4.0f);\r\n    private readonly Vector128&lt;float&gt; _v2 = Vector128.Create(10.0f);\r\n\r\n    [GlobalSetup]\r\n    public void Setup()\r\n    {\r\n        if (!Sse41.IsSupported)\r\n            throw new PlatformNotSupportedException();\r\n    }\r\n\r\n    [Benchmark]\r\n    public Vector128&lt;float&gt; AddToNegative() =&gt;\r\n        Sse41.BlendVariable(_v1, _v1 + _v2, _v1);\r\n}<\/code><\/pre>\n<p>In .NET 11, you get the simpler <code>vblendvps<\/code> form that avoids an unnecessary k-register operation.<\/p>\n<pre><code class=\"language-diff\">; x64\r\n  vmovups   xmm0, [rcx+8]\r\n- vpmovd2m  k1, xmm0\r\n- vaddps    xmm0 {k1}, xmm0, [rcx+18]\r\n+ vaddps    xmm1, xmm0, [rcx+18]\r\n+ vblendvps xmm0, xmm0, xmm1, xmm0\r\n  vmovups   [rdx], xmm0\r\n\r\n; 29 bytes \u2192 24 bytes<\/code><\/pre>\n<p>The masked EVEX form looks more modern, but when the mask originates from a\nvector anyway, the vector-blend sequence is five bytes shorter and avoids\nwriting a mask register. There are only 8 k-registers, and some\nmicroarchitectures have port contention for instructions that write them.<\/p>\n<p>A compiler&#8217;s cost model assigns estimates to operations and instructions, such as their execution cost or throughput and their impact on code size, and uses those estimates to choose between otherwise legal transformations or instruction sequences. Wrong estimates can still produce semantically correct code, just slower or larger code. With <a href=\"https:\/\/github.com\/dotnet\/runtime\/pull\/127048\">dotnet\/runtime#127048<\/a>, which updates the JIT&#8217;s xarch floating-point and SIMD cost model, the JIT&#8217;s cost model reflects modern instruction throughput and encoded size, replacing old x87 assumptions and a flat cost for every intrinsic. That leads to better decisions about common-subexpression elimination and loop unrolling, particularly for 512-bit operations.<\/p>\n<p><a href=\"https:\/\/github.com\/dotnet\/runtime\/pull\/130422\">dotnet\/runtime#130422<\/a> folds a vector lane extraction followed by <code>WithElement<\/code> into one <code>insertps<\/code> that reads the source lane directly. Code such as <code>destination.WithElement(0, source.GetElement(2))<\/code> conceptually extracts a scalar and then inserts it elsewhere. <code>insertps<\/code>, however, has an immediate operand whose bits select both the source lane and destination lane. The JIT can therefore pass the original source vector to the instruction and encode lane 2 in that immediate, instead of first shuffling lane 2 into the scalar position and then inserting it.<\/p>\n<p>Three more xarch changes tighten public SIMD operations on the hardware where they apply. <a href=\"https:\/\/github.com\/dotnet\/runtime\/pull\/125666\">dotnet\/runtime#125666<\/a> from <a href=\"https:\/\/github.com\/alexcovington\">@alexcovington<\/a> replaces the dedicated AVX dot-product instruction with a multiply, add, and permute reduction that has better throughput on contemporary cores:<\/p>\n<pre><code class=\"language-csharp\">\/\/ dotnet run -c Release -f net10.0 --filter \"*\" --runtimes net10.0 net11.0\r\n\r\nusing System.Numerics;\r\nusing System.Runtime.Intrinsics;\r\nusing BenchmarkDotNet.Attributes;\r\nusing BenchmarkDotNet.Running;\r\n\r\nBenchmarkSwitcher.FromAssembly(typeof(Benchmarks).Assembly).Run(args);\r\n\r\n[DisassemblyDiagnoser, HideColumns(\"Job\", \"Error\", \"StdDev\", \"Median\", \"RatioSD\")]\r\npublic class Benchmarks\r\n{\r\n    private readonly Plane _plane = new(new Vector3(1.0f, 2.0f, 3.0f), 4.0f);\r\n    private readonly Vector4 _vector4 = new(5.0f, 6.0f, 7.0f, 8.0f);\r\n    private readonly Quaternion _quaternion1 = new(1.0f, 2.0f, 3.0f, 4.0f);\r\n    private readonly Quaternion _quaternion2 = new(5.0f, 6.0f, 7.0f, 8.0f);\r\n    private readonly Vector128&lt;float&gt; _vector1 = Vector128.Create(1.0f, 2.0f, 3.0f, 4.0f);\r\n    private readonly Vector128&lt;float&gt; _vector2 = Vector128.Create(5.0f, 6.0f, 7.0f, 8.0f);\r\n\r\n    [Benchmark]\r\n    public float PlaneDot() =&gt; Plane.Dot(_plane, _vector4);\r\n\r\n    [Benchmark]\r\n    public float QuaternionDot() =&gt; Quaternion.Dot(_quaternion1, _quaternion2);\r\n\r\n    [Benchmark]\r\n    public float Vector128Dot() =&gt; Vector128.Dot(_vector1, _vector2);\r\n}<\/code><\/pre>\n<table>\n<thead>\n<tr>\n<th>Method<\/th>\n<th>Runtime<\/th>\n<th style=\"text-align: right;\">Mean<\/th>\n<th style=\"text-align: right;\">Ratio<\/th>\n<th style=\"text-align: right;\">Code Size<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td>PlaneDot<\/td>\n<td>.NET 10.0<\/td>\n<td style=\"text-align: right;\">2.616 ns<\/td>\n<td style=\"text-align: right;\">1.00<\/td>\n<td style=\"text-align: right;\">13 B<\/td>\n<\/tr>\n<tr>\n<td>PlaneDot<\/td>\n<td>.NET 11.0<\/td>\n<td style=\"text-align: right;\">1.365 ns<\/td>\n<td style=\"text-align: right;\">0.52<\/td>\n<td style=\"text-align: right;\">31 B<\/td>\n<\/tr>\n<tr>\n<td><\/td>\n<td><\/td>\n<td style=\"text-align: right;\"><\/td>\n<td style=\"text-align: right;\"><\/td>\n<td style=\"text-align: right;\"><\/td>\n<\/tr>\n<tr>\n<td>QuaternionDot<\/td>\n<td>.NET 10.0<\/td>\n<td style=\"text-align: right;\">2.640 ns<\/td>\n<td style=\"text-align: right;\">1.00<\/td>\n<td style=\"text-align: right;\">13 B<\/td>\n<\/tr>\n<tr>\n<td>QuaternionDot<\/td>\n<td>.NET 11.0<\/td>\n<td style=\"text-align: right;\">1.326 ns<\/td>\n<td style=\"text-align: right;\">0.50<\/td>\n<td style=\"text-align: right;\">31 B<\/td>\n<\/tr>\n<tr>\n<td><\/td>\n<td><\/td>\n<td style=\"text-align: right;\"><\/td>\n<td style=\"text-align: right;\"><\/td>\n<td style=\"text-align: right;\"><\/td>\n<\/tr>\n<tr>\n<td>Vector128Dot<\/td>\n<td>.NET 10.0<\/td>\n<td style=\"text-align: right;\">2.597 ns<\/td>\n<td style=\"text-align: right;\">1.00<\/td>\n<td style=\"text-align: right;\">13 B<\/td>\n<\/tr>\n<tr>\n<td>Vector128Dot<\/td>\n<td>.NET 11.0<\/td>\n<td style=\"text-align: right;\">1.366 ns<\/td>\n<td style=\"text-align: right;\">0.53<\/td>\n<td style=\"text-align: right;\">31 B<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<p>Multiplying vectors of bytes is more involved than multiplying vectors of\nlarger integer types because x86 doesn&#8217;t provide a packed byte-multiply\ninstruction. The implementation needs to combine wider 16-bit multiplications\nwhile retaining only the low byte of each product. When it couldn&#8217;t widen the\nwhole operation to the next vector size, .NET 10 split the input into two\nhalves, widened and multiplied each half, narrowed both results, and joined\nthem again. <a href=\"https:\/\/github.com\/dotnet\/runtime\/pull\/126348\">dotnet\/runtime#126348<\/a> from <a href=\"https:\/\/github.com\/saucecontrol\">@saucecontrol<\/a> instead separates the even and odd bytes with masks and shifts, performs two 16-bit multiplications over the full vector width, and recombines the low bytes:<\/p>\n<pre><code class=\"language-csharp\">\/\/ Run on x64 with AVX-512:\r\n\/\/ dotnet run -c Release -f net10.0 --filter \"*\" --runtimes net10.0 net11.0\r\n\r\nusing System.Runtime.Intrinsics;\r\nusing BenchmarkDotNet.Attributes;\r\nusing BenchmarkDotNet.Running;\r\n\r\nBenchmarkSwitcher.FromAssembly(typeof(Benchmarks).Assembly).Run(args);\r\n\r\n[DisassemblyDiagnoser, HideColumns(\"Job\", \"Error\", \"StdDev\", \"Median\", \"RatioSD\")]\r\npublic class Benchmarks\r\n{\r\n    private readonly Vector512&lt;byte&gt; _left = Vector512.Create((byte)17);\r\n    private readonly Vector512&lt;byte&gt; _right = Vector512.Create((byte)19);\r\n\r\n    [Benchmark]\r\n    public Vector512&lt;byte&gt; Multiply() =&gt; _left * _right;\r\n}<\/code><\/pre>\n<table>\n<thead>\n<tr>\n<th>Method<\/th>\n<th>Runtime<\/th>\n<th style=\"text-align: right;\">Mean<\/th>\n<th style=\"text-align: right;\">Ratio<\/th>\n<th style=\"text-align: right;\">Code Size<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td>Multiply<\/td>\n<td>.NET 10.0<\/td>\n<td style=\"text-align: right;\">3.752 ns<\/td>\n<td style=\"text-align: right;\">1.00<\/td>\n<td style=\"text-align: right;\">114 B<\/td>\n<\/tr>\n<tr>\n<td>Multiply<\/td>\n<td>.NET 11.0<\/td>\n<td style=\"text-align: right;\">2.174 ns<\/td>\n<td style=\"text-align: right;\">0.58<\/td>\n<td style=\"text-align: right;\">73 B<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<p>The .NET 11 sequence no longer extracts, widens, narrows, and reinserts both\n256-bit halves:<\/p>\n<pre><code class=\"language-diff\">; x64\r\n vmovups     zmm0, [rcx+8]\r\n-vmovaps     zmm1, zmm0\r\n-vpmovzxbw   zmm1, ymm1\r\n-vmovups     zmm2, [rcx+48]\r\n-vmovaps     zmm3, zmm2\r\n-vpmovzxbw   zmm3, ymm3\r\n-vpmullw     zmm1, zmm3, zmm1\r\n-vpmovwb     ymm1, zmm1\r\n-vextracti32x8 ymm0, zmm0, 1\r\n-vpmovzxbw   zmm0, ymm0\r\n-vextracti32x8 ymm2, zmm2, 1\r\n-vpmovzxbw   zmm2, ymm2\r\n-vpmullw     zmm0, zmm2, zmm0\r\n-vpmovwb     ymm0, zmm0\r\n-vinserti32x8 zmm0, zmm1, ymm0, 1\r\n+vmovups     zmm1, [rcx+48]\r\n+vpmullw     zmm2, zmm0, zmm1\r\n+vpsrlw      zmm0, zmm0, 8\r\n+vpandd      zmm1, zmm1, dword bcst [RWD00]\r\n+vpmullw     zmm0, zmm1, zmm0\r\n+vpternlogd  zmm0, zmm2, dword bcst [RWD04], 0F8\r\n vmovups     [rdx], zmm0\r\n\r\n; 114 bytes \u2192 73 bytes<\/code><\/pre>\n<p><a href=\"https:\/\/github.com\/dotnet\/runtime\/pull\/127094\">dotnet\/runtime#127094<\/a> lets scalar conversions between <code>Half<\/code> and <code>float<\/code> use F16C&#8217;s <code>vcvtps2ph<\/code> and <code>vcvtph2ps<\/code> instructions when AVX2 is enabled:<\/p>\n<pre><code class=\"language-csharp\">\/\/ Run on x64 with AVX2 enabled:\r\n\/\/ dotnet run -c Release -f net10.0 --filter \"*\" --runtimes net10.0 net11.0\r\n\r\nusing BenchmarkDotNet.Attributes;\r\nusing BenchmarkDotNet.Running;\r\n\r\nBenchmarkSwitcher.FromAssembly(typeof(Benchmarks).Assembly).Run(args);\r\n\r\n[DisassemblyDiagnoser, HideColumns(\"Job\", \"Error\", \"StdDev\", \"Median\", \"RatioSD\")]\r\npublic class Benchmarks\r\n{\r\n    private Half _half = (Half)123.5f;\r\n    private float _single = 123.5f;\r\n\r\n    [Benchmark] public float HalfToSingle() =&gt; (float)_half;\r\n    [Benchmark] public Half SingleToHalf() =&gt; (Half)_single;\r\n}<\/code><\/pre>\n<table>\n<thead>\n<tr>\n<th>Method<\/th>\n<th>Runtime<\/th>\n<th style=\"text-align: right;\">Mean<\/th>\n<th style=\"text-align: right;\">Ratio<\/th>\n<th style=\"text-align: right;\">Code Size<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td>HalfToSingle<\/td>\n<td>.NET 10.0<\/td>\n<td style=\"text-align: right;\">2.506 ns<\/td>\n<td style=\"text-align: right;\">1.00<\/td>\n<td style=\"text-align: right;\">104 B<\/td>\n<\/tr>\n<tr>\n<td>HalfToSingle<\/td>\n<td>.NET 11.0<\/td>\n<td style=\"text-align: right;\">1.380 ns<\/td>\n<td style=\"text-align: right;\">0.55<\/td>\n<td style=\"text-align: right;\">14 B<\/td>\n<\/tr>\n<tr>\n<td><\/td>\n<td><\/td>\n<td style=\"text-align: right;\"><\/td>\n<td style=\"text-align: right;\"><\/td>\n<td style=\"text-align: right;\"><\/td>\n<\/tr>\n<tr>\n<td>SingleToHalf<\/td>\n<td>.NET 10.0<\/td>\n<td style=\"text-align: right;\">2.598 ns<\/td>\n<td style=\"text-align: right;\">1.00<\/td>\n<td style=\"text-align: right;\">134 B<\/td>\n<\/tr>\n<tr>\n<td>SingleToHalf<\/td>\n<td>.NET 11.0<\/td>\n<td style=\"text-align: right;\">1.351 ns<\/td>\n<td style=\"text-align: right;\">0.52<\/td>\n<td style=\"text-align: right;\">19 B<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<p>Finally, <a href=\"https:\/\/github.com\/dotnet\/runtime\/pull\/127536\">dotnet\/runtime#127536<\/a> from <a href=\"https:\/\/github.com\/Ruihan-Yin\">@Ruihan-Yin<\/a> completes support for APX, Intel&#8217;s Advanced Performance Extensions. In addition to expanding the general-purpose register set, APX adds forms of many instructions that don&#8217;t overwrite the processor&#8217;s condition flags. That gives the register allocator and instruction scheduler more freedom to keep values and pending conditions alive at the same time. Its <code>CTEST<\/code> and <code>CFCMOV<\/code> instructions can also represent chained conditions without branches and replace some compare-with-zero forms with shorter encodings. Applications don&#8217;t need to call APX-specific APIs to benefit; when the hardware and operating system expose APX, the JIT is able to utilize the additional instructions automatically.<\/p>\n<p>On Arm64, the work in .NET 11 spans both conventional code generation and\ncontinued support for SVE (Scalable Vector Extension). Unlike 128-bit AdvSimd\nvectors, an SVE vector doesn&#8217;t have one width fixed by the instruction set;\neach processor chooses a supported width, and the same compiled loop uses\npredicate masks to operate on however many elements fit. That makes SVE well\nsuited to loops whose trip counts are not exact multiples of a particular\nvector size.<\/p>\n<p><a href=\"https:\/\/github.com\/dotnet\/runtime\/pull\/121986\">dotnet\/runtime#121986<\/a> improves zeroing for larger stack allocations on Arm64. The JIT can store two zeroed 128-bit vector registers at a time, doubling the amount cleared by each instruction:<\/p>\n<pre><code class=\"language-csharp\">\/\/ dotnet run -c Release -f net10.0 --filter \"*\" --runtimes net10.0 net11.0\r\n\r\nusing BenchmarkDotNet.Attributes;\r\nusing BenchmarkDotNet.Running;\r\nusing System.Runtime.CompilerServices;\r\nusing System.Runtime.Intrinsics.Arm;\r\n\r\nBenchmarkSwitcher.FromAssembly(typeof(Benchmarks).Assembly).Run(args);\r\n\r\n[HideColumns(\"Job\", \"Error\", \"StdDev\", \"Median\", \"RatioSD\")]\r\npublic class Benchmarks\r\n{\r\n    [Benchmark] public void Stackalloc512() =&gt; Consume(stackalloc byte[512]);\r\n    [Benchmark] public void Stackalloc1024() =&gt; Consume(stackalloc byte[1024]);\r\n    [Benchmark] public void Stackalloc16384() =&gt; Consume(stackalloc byte[16384]);\r\n\r\n    [MethodImpl(MethodImplOptions.NoInlining)]\r\n    private static void Consume(Span&lt;byte&gt; x) { }\r\n}<\/code><\/pre>\n<table>\n<thead>\n<tr>\n<th>Method<\/th>\n<th>Runtime<\/th>\n<th>Mean<\/th>\n<th>Ratio<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td>Stackalloc512<\/td>\n<td>.NET 10.0<\/td>\n<td>13.65 ns<\/td>\n<td>1.00<\/td>\n<\/tr>\n<tr>\n<td>Stackalloc512<\/td>\n<td>.NET 11.0<\/td>\n<td>9.557 ns<\/td>\n<td>0.70<\/td>\n<\/tr>\n<tr>\n<td>Stackalloc1024<\/td>\n<td>.NET 10.0<\/td>\n<td>25.35 ns<\/td>\n<td>1.00<\/td>\n<\/tr>\n<tr>\n<td>Stackalloc1024<\/td>\n<td>.NET 11.0<\/td>\n<td>14.332 ns<\/td>\n<td>0.57<\/td>\n<\/tr>\n<tr>\n<td>Stackalloc16384<\/td>\n<td>.NET 10.0<\/td>\n<td>312.97 ns<\/td>\n<td>1.00<\/td>\n<\/tr>\n<tr>\n<td>Stackalloc16384<\/td>\n<td>.NET 11.0<\/td>\n<td>162.656 ns<\/td>\n<td>0.52<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<p>A wave of smaller Arm64 changes improves instruction selection. In .NET 11, <a href=\"https:\/\/github.com\/dotnet\/runtime\/pull\/119758\">dotnet\/runtime#119758<\/a> from <a href=\"https:\/\/github.com\/jonathandavies-arm\">@jonathandavies-arm<\/a> lets a comparison with zero consume condition flags set as a side effect of the preceding arithmetic or logical instruction, avoiding a separate <code>cmp<\/code>. <a href=\"https:\/\/github.com\/dotnet\/runtime\/pull\/123138\">dotnet\/runtime#123138<\/a> from <a href=\"https:\/\/github.com\/jonathandavies-arm\">@jonathandavies-arm<\/a> recognizes bit-extraction idioms such as <code>(value &gt;&gt; 6) &amp; 0x3F<\/code> and maps them to the dedicated <code>ubfx<\/code> instruction. And <a href=\"https:\/\/github.com\/dotnet\/runtime\/pull\/123546\">dotnet\/runtime#123546<\/a> from <a href=\"https:\/\/github.com\/jonathandavies-arm\">@jonathandavies-arm<\/a> removes a non-overflowing <code>int<\/code>-to-<code>long<\/code> widening cast when the result is immediately truncated to a smaller integer type.<\/p>\n<pre><code class=\"language-csharp\">\/\/ dotnet run -c Release -f net10.0 --filter \"*\" --runtimes net10.0 net11.0\r\n\r\nusing BenchmarkDotNet.Attributes;\r\nusing BenchmarkDotNet.Running;\r\n\r\nBenchmarkSwitcher.FromAssembly(typeof(Benchmarks).Assembly).Run(args);\r\n\r\n[DisassemblyDiagnoser, HideColumns(\"Job\", \"Error\", \"StdDev\", \"Median\", \"RatioSD\", \"left\", \"right\", \"value\")]\r\npublic class Benchmarks\r\n{\r\n    [Benchmark]\r\n    [Arguments(-1, 2)]\r\n    public bool CompareWithZero(int left, int right) =&gt; (left &amp; right) &lt;= 0;\r\n\r\n    [Benchmark]\r\n    [Arguments(0x7F65_4321)]\r\n    public int ExtractBits(int value) =&gt; (value &gt;&gt; 6) &amp; 0x3F;\r\n\r\n    [Benchmark]\r\n    [Arguments(0x1122_3344)]\r\n    public sbyte TruncateAfterWidening(int value) =&gt; (sbyte)(long)value;\r\n}<\/code><\/pre>\n<p>Each example removes one instruction. <code>CompareWithZero<\/code> changes <code>and<\/code> to its flag-setting <code>ands<\/code> form and drops the subsequent <code>cmp<\/code>; <code>ExtractBits<\/code> replaces a shift and mask with <code>ubfx<\/code>; and <code>TruncateAfterWidening<\/code> drops the <code>sxtw<\/code> that widened the value to 64 bits only for <code>sxtb<\/code> to immediately truncate it again:<\/p>\n<pre><code class=\"language-diff\">; Arm64\r\n; CompareWithZero: 28 bytes \u2192 24 bytes\r\n-            and     w0, w1, w2\r\n-            cmp     w0, #0\r\n+            ands    w0, w1, w2\r\n             cset    x0, le\r\n\r\n; ExtractBits: 24 bytes \u2192 20 bytes\r\n-            asr     w0, w1, #6\r\n-            and     w0, w0, #63\r\n+            ubfx    w0, w1, #6, #6\r\n\r\n; TruncateAfterWidening: 24 bytes \u2192 20 bytes\r\n-            sxtw    x0, w1\r\n-            sxtb    w0, w0\r\n+            sxtb    w0, w1<\/code><\/pre>\n<p>Instruction selection also improves where values move between registers and memory. In .NET 11, <a href=\"https:\/\/github.com\/dotnet\/runtime\/pull\/126803\">dotnet\/runtime#126803<\/a> changes <code>ToScalar<\/code> on a vector of 64-bit integers to use <code>fmov Xd, Dn<\/code> rather than the lane-extract instruction <code>umov<\/code>; in both cases lane zero moves to a general-purpose register, but <code>fmov<\/code> is the more direct form. For ReadyToRun code, <a href=\"https:\/\/github.com\/dotnet\/runtime\/pull\/129589\">dotnet\/runtime#129589<\/a> folds relocatable indirection-cell loads from <code>adrp + add + ldr<\/code> into <code>adrp + ldr #:lo12:<\/code>, removing the separate address addition. And <a href=\"https:\/\/github.com\/dotnet\/runtime\/pull\/129932\">dotnet\/runtime#129932<\/a> re-enables <code>ldp<\/code>\/<code>stp<\/code> formation for negative unscaled offsets, letting two adjacent loads or stores become one paired instruction.<\/p>\n<p>The first and third changes are easy to see with small methods:<\/p>\n<pre><code class=\"language-csharp\">\/\/ dotnet run -c Release -f net10.0 --filter \"*\" --runtimes net10.0 net11.0\r\n\r\nusing BenchmarkDotNet.Attributes;\r\nusing BenchmarkDotNet.Running;\r\nusing System.Runtime.CompilerServices;\r\nusing System.Runtime.Intrinsics;\r\n\r\nBenchmarkSwitcher.FromAssembly(typeof(Benchmarks).Assembly).Run(args);\r\n\r\n[DisassemblyDiagnoser, HideColumns(\"Job\", \"Error\", \"StdDev\", \"Median\", \"RatioSD\")]\r\npublic class Benchmarks\r\n{\r\n    private Vector128&lt;long&gt; _vector = Vector128.Create(42L, 84L);\r\n    private nint[] _storage = new nint[8];\r\n\r\n    [Benchmark]\r\n    public long ToScalar() =&gt; ToScalarCore(_vector);\r\n\r\n    [Benchmark]\r\n    public void ClearPrevious() =&gt; ClearPreviousCore(ref _storage[4]);\r\n\r\n    [MethodImpl(MethodImplOptions.NoInlining)]\r\n    private static long ToScalarCore(Vector128&lt;long&gt; value) =&gt; value.ToScalar();\r\n\r\n    [MethodImpl(MethodImplOptions.NoInlining)]\r\n    private static void ClearPreviousCore(ref nint value)\r\n    {\r\n        Unsafe.Add(ref value, -1) = 0;\r\n        Unsafe.Add(ref value, -2) = 0;\r\n        Unsafe.Add(ref value, -3) = 0;\r\n        Unsafe.Add(ref value, -4) = 0;\r\n    }\r\n}<\/code><\/pre>\n<p>The <code>ToScalar<\/code> change is a direct instruction substitution, while the negative-offset stores collapse from four instructions to two, reducing the helper from 32 bytes to 24 bytes:<\/p>\n<pre><code class=\"language-diff\">; Arm64\r\n; ToScalarCore\r\n-            umov    x0, v0.d[0]\r\n+            fmov    x0, d0\r\n\r\n; ClearPreviousCore\r\n-            str     xzr, [x0, #-0x08]\r\n-            str     xzr, [x0, #-0x10]\r\n-            str     xzr, [x0, #-0x18]\r\n-            str     xzr, [x0, #-0x20]\r\n+            stp     xzr, xzr, [x0, #-0x10]\r\n+            stp     xzr, xzr, [x0, #-0x20]<\/code><\/pre>\n<p>Bit-counting operations benefit as well. <code>PopCount<\/code> counts the one bits in a value, while <code>TrailingZeroCount<\/code> counts the zero bits below its least-significant one bit. <a href=\"https:\/\/github.com\/dotnet\/runtime\/pull\/128677\">dotnet\/runtime#128677<\/a> imports both as dedicated Arm64 intrinsics, making their intent visible to later optimization. On processors with the FEAT_CSSC extension, <a href=\"https:\/\/github.com\/dotnet\/runtime\/pull\/130332\">dotnet\/runtime#130332<\/a> can then lower them directly to the scalar <code>cnt<\/code> and <code>ctz<\/code> instructions.<\/p>\n<p>Comparison masks are another place where spelling out the intent enables much better code. Portable SIMD code often compares vectors, calls <code>ExtractMostSignificantBits<\/code>, and then asks whether any lane matched, counts matching lanes, or finds the first or last match. <a href=\"https:\/\/github.com\/dotnet\/runtime\/pull\/129688\">dotnet\/runtime#129688<\/a> from <a href=\"https:\/\/github.com\/jonathandavies-arm\">@jonathandavies-arm<\/a> recognizes those consumers on Arm64 and avoids materializing the full scalar mask: it can horizontally reduce the vector mask directly.<\/p>\n<pre><code class=\"language-csharp\">\/\/ dotnet run -c Release -f net10.0 --filter \"*\" --runtimes net10.0 net11.0\r\n\r\nusing BenchmarkDotNet.Attributes;\r\nusing BenchmarkDotNet.Running;\r\nusing System.Numerics;\r\nusing System.Runtime.CompilerServices;\r\nusing System.Runtime.Intrinsics;\r\n\r\nBenchmarkSwitcher.FromAssembly(typeof(Benchmarks).Assembly).Run(args);\r\n\r\n[DisassemblyDiagnoser, HideColumns(\"Job\", \"Error\", \"StdDev\", \"Median\", \"RatioSD\")]\r\npublic class Benchmarks\r\n{\r\n    private Vector128&lt;int&gt; _value = Vector128.Create(1, -2, 3, -4);\r\n\r\n    [Benchmark]\r\n    public bool AnyLessThan() =&gt; AnyLessThanCore(_value, 0);\r\n\r\n    [Benchmark]\r\n    public int CountLessThan() =&gt; CountLessThanCore(_value, 0);\r\n\r\n    [MethodImpl(MethodImplOptions.NoInlining)]\r\n    private static bool AnyLessThanCore(Vector128&lt;int&gt; value, int limit) =&gt;\r\n        Vector128.LessThan(value, Vector128.Create(limit))\r\n            .ExtractMostSignificantBits() != 0;\r\n\r\n    [MethodImpl(MethodImplOptions.NoInlining)]\r\n    private static int CountLessThanCore(Vector128&lt;int&gt; value, int limit) =&gt;\r\n        BitOperations.PopCount(\r\n            Vector128.LessThan(value, Vector128.Create(limit))\r\n                .ExtractMostSignificantBits());\r\n}<\/code><\/pre>\n<p>In .NET 10, both helpers first pack the most-significant bit from every comparison lane into a scalar. .NET 11 instead keeps the comparison as a vector.<\/p>\n<pre><code class=\"language-diff\">; Arm64\r\n; AnyLessThanCore\r\n             cmgt    v16.4s, v16.4s, v0.4s\r\n-            movi    v17.4s, #0x80, LSL #24\r\n-            and     v16.4s, v16.4s, v17.4s\r\n-            ldr     q17, [@RWD00]\r\n-            ushl    v16.4s, v16.4s, v17.4s\r\n-            addv    s16, v16.4s\r\n-            smov    x0, v16.s[0]\r\n+            umaxv   s16, v16.4s\r\n+            umov    w0, v16.s[0]\r\n             cmp     w0, #0\r\n             cset    x0, ne\r\n\r\n; CountLessThanCore\r\n             cmgt    v16.4s, v16.4s, v0.4s\r\n-            movi    v17.4s, #0x80, LSL #24\r\n-            and     v16.4s, v16.4s, v17.4s\r\n-            ldr     q17, [@RWD00]\r\n-            ushl    v16.4s, v16.4s, v17.4s\r\n-            addv    s16, v16.4s\r\n-            movi    v17.2s, #0\r\n-            smov    x0, v16.s[0]\r\n-            ins     v17.s[0], w0\r\n-            cnt     v16.8b, v17.8b\r\n-            addv    b16, v16.8b\r\n-            umov    w0, v16.b[0]\r\n+            ushr    v16.4s, v16.4s, #31\r\n+            addv    s16, v16.4s\r\n+            umov    w0, v16.s[0]<\/code><\/pre>\n<p>On the SVE and SVE2 side, <a href=\"https:\/\/github.com\/dotnet\/runtime\/pull\/129852\">dotnet\/runtime#129852<\/a> from <a href=\"https:\/\/github.com\/snickolls-arm\">@snickolls-arm<\/a> removes the old 128-bit size ceiling for <code>Vector&lt;T&gt;<\/code> on Arm64 and lets the runtime size the type from the process&#8217;s actual SVE vector length. (Scalable <code>Vector&lt;T&gt;<\/code> remains experimental and disabled by default in .NET 11, so this expands what the experimental mode can do; it doesn&#8217;t speed up the default <code>Vector&lt;T&gt;<\/code> configuration.)<\/p>\n<p>The public intrinsic surface also grows. In .NET 11, <a href=\"https:\/\/github.com\/dotnet\/runtime\/pull\/118957\">dotnet\/runtime#118957<\/a> from <a href=\"https:\/\/github.com\/SwapnilGaikwad\">@SwapnilGaikwad<\/a> exposes odd-lane floating-point conversions; &#8220;odd lane&#8221; here means converting elements 1, 3, 5, and so on, which is useful when widening or narrowing interleaved data. <a href=\"https:\/\/github.com\/dotnet\/runtime\/pull\/123890\">dotnet\/runtime#123890<\/a> from <a href=\"https:\/\/github.com\/ylpoonlg\">@ylpoonlg<\/a> and <a href=\"https:\/\/github.com\/dotnet\/runtime\/pull\/123892\">dotnet\/runtime#123892<\/a> from <a href=\"https:\/\/github.com\/ylpoonlg\">@ylpoonlg<\/a> add non-temporal gather loads and scatter stores, which read from or write to multiple non-contiguous addresses (the &#8220;gather&#8221; part) while hinting that the data need not remain in cache (the &#8220;non-temporal&#8221; part).<\/p>\n<p>Other changes improve the predicates that make scalable loops work. <a href=\"https:\/\/github.com\/dotnet\/runtime\/pull\/127538\">dotnet\/runtime#127538<\/a> adds hardware-generated predicate masks for more loop and memory-access patterns, while <a href=\"https:\/\/github.com\/dotnet\/runtime\/pull\/126398\">dotnet\/runtime#126398<\/a> from <a href=\"https:\/\/github.com\/ylpoonlg\">@ylpoonlg<\/a> reduces setup moves for masked operations. And <a href=\"https:\/\/github.com\/dotnet\/runtime\/pull\/128326\">dotnet\/runtime#128326<\/a> from <a href=\"https:\/\/github.com\/snickolls-arm\">@snickolls-arm<\/a> improves how SVE masks flow through the JIT, allowing zeroing forms of instructions to replace separate constant setup. <a href=\"https:\/\/github.com\/dotnet\/runtime\/pull\/127520\">dotnet\/runtime#127520<\/a> from <a href=\"https:\/\/github.com\/a74nh\">@a74nh<\/a> enables scalable vector and mask constants, and <a href=\"https:\/\/github.com\/dotnet\/runtime\/pull\/128148\">dotnet\/runtime#128148<\/a> from <a href=\"https:\/\/github.com\/snickolls-arm\">@snickolls-arm<\/a> uses vector stores to initialize scalable vector locals, replacing scalar loops.<\/p>\n<h3>Register Allocation<\/h3>\n<p>Generated code constantly moves values between the CPU&#8217;s limited set of fast registers and temporary stack slots. Register allocation in a compiler decides which values stay in registers and which are &#8220;spilled&#8221; to the stack; avoiding one spill can remove both the store and the later reload.<\/p>\n<p>Some small structs are passed with multiple fields packed into one register. In .NET 11, <a href=\"https:\/\/github.com\/dotnet\/runtime\/pull\/112740\">dotnet\/runtime#112740<\/a> lets the JIT extract those fields directly, avoiding a &#8220;spill&#8221; to a temporary stack slot followed by a reload of each field:<\/p>\n<pre><code class=\"language-csharp\">\/\/ dotnet run -c Release -f net10.0 --filter \"*\" --runtimes net10.0 net11.0\r\n\r\nusing BenchmarkDotNet.Attributes;\r\nusing BenchmarkDotNet.Running;\r\nusing System.Drawing;\r\nusing System.Runtime.CompilerServices;\r\n\r\nBenchmarkSwitcher.FromAssembly(typeof(Benchmarks).Assembly).Run(args);\r\n\r\n[HideColumns(\"Job\", \"Error\", \"StdDev\", \"Median\", \"RatioSD\")]\r\npublic class Benchmarks\r\n{\r\n    private readonly Memory&lt;int&gt;[] _memories = CreateMemories();\r\n\r\n    private static Memory&lt;int&gt;[] CreateMemories()\r\n    {\r\n        Random rng = new(42);\r\n        var memories = new Memory&lt;int&gt;[4096];\r\n        for (int i = 0; i &lt; memories.Length; i++)\r\n            memories[i] = new int[rng.Next(0, 20)];\r\n\r\n        return memories;\r\n    }\r\n\r\n    [MethodImpl(MethodImplOptions.NoInlining)]\r\n    private static bool Test(Memory&lt;int&gt; mem) =&gt; mem.Length &gt; 10;\r\n\r\n    [Benchmark]\r\n    public int MemoryLengthExtract_Loop()\r\n    {\r\n        int count = 0;\r\n        for (int i = 0; i &lt; _memories.Length; i++)\r\n            if (Test(_memories[i]))\r\n                count++;\r\n\r\n        return count;\r\n    }\r\n}<\/code><\/pre>\n<p>The measured row uses <code>Memory&lt;int&gt;<\/code> because its length arrives packed into part of an argument register on Arm64. The new extraction avoids a stack round-trip on every call.<\/p>\n<table>\n<thead>\n<tr>\n<th>Method<\/th>\n<th>Runtime<\/th>\n<th>Mean<\/th>\n<th>Ratio<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td>MemoryLengthExtract_Loop<\/td>\n<td>.NET 10.0<\/td>\n<td>27.15 \u03bcs<\/td>\n<td>1.00<\/td>\n<\/tr>\n<tr>\n<td>MemoryLengthExtract_Loop<\/td>\n<td>.NET 11.0<\/td>\n<td>23.95 \u03bcs<\/td>\n<td>0.88<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<p>Two broader register-allocation changes reduce unnecessary copies and spills: <a href=\"https:\/\/github.com\/dotnet\/runtime\/pull\/125214\">dotnet\/runtime#125214<\/a> handles more conflicts directly, while <a href=\"https:\/\/github.com\/dotnet\/runtime\/pull\/125219\">dotnet\/runtime#125219<\/a> steers short-lived values away from registers an upcoming operation will overwrite. <a href=\"https:\/\/github.com\/dotnet\/runtime\/pull\/126552\">dotnet\/runtime#126552<\/a> from <a href=\"https:\/\/github.com\/SingleAccretion\">@SingleAccretion<\/a> removes an old restriction on method prologs, eliminating jumps that existed only to satisfy that encoding rule.<\/p>\n<h3>Write Barriers and Garbage Collection<\/h3>\n<p>The .NET garbage collector is generational: new objects start in gen0, while objects that survive collections are promoted to gen1 and gen2. That enables the GC to collect younger generations without having to scan the whole heap. Of course, a reference to a younger object could get written to a field of an older one, in which case only scanning the younger generation would lead to problems. To ensure such references aren&#8217;t missed, whenever a write could create one, the JIT emits a small piece of code to update the GC&#8217;s bookkeeping; that code is known as a GC write barrier. Reference writes happen a lot, so it&#8217;s really important for performance that those barriers be as cheap as possible, and elided if they&#8217;re provably not needed at all.<\/p>\n<p>Managed reference stores may require both an array covariance check and a GC write barrier. Arrays in .NET are covariant, meaning a <code>TDerived[]<\/code> can be used as a <code>TBase[]<\/code>, e.g. a <code>string[]<\/code> can be used as an <code>object[]<\/code>; consequently, storing an instance into an <code>object[]<\/code> must validate that the instance is actually of the right type (otherwise, you could have a <code>TDerived1[]<\/code> masquerading as a <code>TBase[]<\/code> and try to store a <code>TDerived2<\/code> into it, which would cause badness if it were to store successfully). <a href=\"https:\/\/github.com\/dotnet\/runtime\/pull\/126547\">dotnet\/runtime#126547<\/a> expands calls to the runtime&#8217;s array-store helper into the individual operations it performs, exposing both the covariance check and write barrier to the JIT. When the JIT knows the array&#8217;s exact type, it can then eliminate the covariance check and optimize the barrier:<\/p>\n<pre><code class=\"language-csharp\">\/\/ dotnet run -c Release -f net10.0 --filter \"*\" --runtimes net10.0 net11.0\r\n\r\nusing BenchmarkDotNet.Attributes;\r\nusing BenchmarkDotNet.Running;\r\nusing System.Runtime.CompilerServices;\r\n\r\nBenchmarkSwitcher.FromAssembly(typeof(Benchmarks).Assembly).Run(args);\r\n\r\n[HideColumns(\"Job\", \"Error\", \"StdDev\", \"Median\", \"RatioSD\")]\r\npublic class Benchmarks\r\n{\r\n    private readonly object[] _array = new object[4096];\r\n    private object _value = new();\r\n\r\n    [MethodImpl(MethodImplOptions.NoInlining)]\r\n    private static void StoreAll(object[] arr, object value)\r\n    {\r\n        for (int i = 0; i &lt; arr.Length; i++)\r\n            arr[i] = value;\r\n    }\r\n\r\n    [Benchmark]\r\n    public object[] CovariantStore_Loop()\r\n    {\r\n        StoreAll(_array, _value);\r\n        return _array;\r\n    }\r\n}<\/code><\/pre>\n<table>\n<thead>\n<tr>\n<th>Method<\/th>\n<th>Runtime<\/th>\n<th>Mean<\/th>\n<th>Ratio<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td>CovariantStore_Loop<\/td>\n<td>.NET 10.0<\/td>\n<td>10.85 \u03bcs<\/td>\n<td>1.00<\/td>\n<\/tr>\n<tr>\n<td>CovariantStore_Loop<\/td>\n<td>.NET 11.0<\/td>\n<td>6.042 \u03bcs<\/td>\n<td>0.56<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<p>Sometimes writes are done one at a time, but sometimes they can be batched, as happens when copying structs. <a href=\"https:\/\/github.com\/dotnet\/runtime\/pull\/128238\">dotnet\/runtime#128238<\/a> extends the JIT&#8217;s heap-destination analysis from individual stores to whole-struct copies. <a href=\"https:\/\/github.com\/dotnet\/runtime\/pull\/128542\">dotnet\/runtime#128542<\/a> then replaces a specialized helper that copied one reference field at a time with reference stores and vector stores for the non-reference data. Together, they let the JIT choose more efficient write barriers and copy the rest of a mixed struct with SIMD.<\/p>\n<pre><code class=\"language-csharp\">\/\/ dotnet run -c Release -f net10.0 --filter \"*\" --runtimes net10.0 net11.0\r\n\r\nusing BenchmarkDotNet.Attributes;\r\nusing BenchmarkDotNet.Running;\r\nusing System.Runtime.CompilerServices;\r\n\r\nBenchmarkSwitcher.FromAssembly(typeof(Benchmarks).Assembly).Run(args);\r\n\r\n[HideColumns(\"Job\", \"Error\", \"StdDev\", \"Median\", \"RatioSD\")]\r\npublic class Benchmarks\r\n{\r\n    [InlineArray(4)]\r\n    public struct InlineArray4Long\r\n    {\r\n        private long _element0;\r\n    }\r\n\r\n    public struct MyStruct\r\n    {\r\n        public string A;\r\n        public InlineArray4Long G;\r\n        public string B;\r\n    }\r\n\r\n    private MyStruct _src;\r\n    private MyStruct _dst;\r\n\r\n    [GlobalSetup]\r\n    public void Setup()\r\n    {\r\n        _src = new MyStruct { A = \"hello\", B = \"world\" };\r\n        _src.G[0] = 1;\r\n        _src.G[1] = 2;\r\n        _src.G[2] = 3;\r\n        _src.G[3] = 4;\r\n    }\r\n\r\n    [Benchmark]\r\n    public void HeapStructCopy() =&gt; _dst = _src;\r\n}<\/code><\/pre>\n<table>\n<thead>\n<tr>\n<th>Method<\/th>\n<th>Runtime<\/th>\n<th>Mean<\/th>\n<th>Ratio<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td>HeapStructCopy<\/td>\n<td>.NET 10.0<\/td>\n<td>4.132 ns<\/td>\n<td>1.00<\/td>\n<\/tr>\n<tr>\n<td>HeapStructCopy<\/td>\n<td>.NET 11.0<\/td>\n<td>3.071 ns<\/td>\n<td>0.74<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<p><a href=\"https:\/\/github.com\/dotnet\/runtime\/pull\/130535\">dotnet\/runtime#130535<\/a> handles the equivalent case for small structs that don&#8217;t contain object references. Once the JIT has turned the copy into several writes to adjacent fields, it can combine them into fewer, wider writes.<\/p>\n<pre><code class=\"language-csharp\">\/\/ dotnet run -c Release -f net10.0 --filter \"*\" --runtimes net10.0 net11.0\r\n\r\nusing BenchmarkDotNet.Attributes;\r\nusing BenchmarkDotNet.Running;\r\nusing System.Runtime.CompilerServices;\r\n\r\nBenchmarkSwitcher.FromAssembly(typeof(Benchmarks).Assembly).Run(args);\r\n\r\n[DisassemblyDiagnoser, HideColumns(\"Job\", \"Error\", \"StdDev\", \"Median\", \"RatioSD\")]\r\npublic class Benchmarks\r\n{\r\n    private Int128 _value;\r\n\r\n    [Benchmark]\r\n    public void StoreInt128() =&gt; _value = 123456789;\r\n}<\/code><\/pre>\n<p>.NET 10 stores the low and high halves separately. .NET 11 loads the value into a vector register and writes all 16 bytes at once.<\/p>\n<pre><code class=\"language-diff\">; x64\r\n; StoreInt128\r\n-       mov      qword ptr [rcx+8], 75BCD15\r\n-       xor      eax, eax\r\n-       mov      [rcx+10], rax\r\n+       vmovss   xmm0, dword ptr [RWD00]\r\n+       vmovups  [rcx+8], xmm0\r\n\r\n-; Total bytes of code 15\r\n+; Total bytes of code 14<\/code><\/pre>\n<p>The same idea applies when the source code assigns neighboring fields\nindividually. <a href=\"https:\/\/github.com\/dotnet\/runtime\/pull\/126562\">dotnet\/runtime#126562<\/a>\nenables this for promoted struct locals, while\n<a href=\"https:\/\/github.com\/dotnet\/runtime\/pull\/130107\">dotnet\/runtime#130107<\/a>\nextends it to adjacent fields at constant static addresses:<\/p>\n<pre><code class=\"language-csharp\">\/\/ dotnet run -c Release -f net11.0 --filter \"*\"\r\n\r\nusing BenchmarkDotNet.Attributes;\r\nusing BenchmarkDotNet.Running;\r\nusing System.Runtime.CompilerServices;\r\n\r\nBenchmarkSwitcher.FromAssembly(typeof(Benchmarks).Assembly).Run(args);\r\n\r\n[DisassemblyDiagnoser, HideColumns(\"Job\", \"Error\", \"StdDev\", \"Median\", \"RatioSD\")]\r\npublic class Benchmarks\r\n{\r\n    private static Point s_point;\r\n\r\n    [Benchmark]\r\n    public void SetPoint() =&gt; Set();\r\n\r\n    [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.AggressiveOptimization)]\r\n    private static void Set()\r\n    {\r\n        s_point.X = 1;\r\n        s_point.Y = 2;\r\n    }\r\n\r\n    private struct Point\r\n    {\r\n        public int X;\r\n        public int Y;\r\n    }\r\n}<\/code><\/pre>\n<p>The referenced .NET 11 x64 build combines the two 32-bit constants and writes\nboth fields with one 64-bit store:<\/p>\n<pre><code class=\"language-asm\">; x64\r\nmov     rax, 200000001\r\nmov     rcx, &lt;address of s_point&gt;\r\nmov     [rcx], rax<\/code><\/pre>\n<p><a href=\"https:\/\/github.com\/dotnet\/runtime\/pull\/127487\">dotnet\/runtime#127487<\/a> applies a related improvement when stack protection requires a struct parameter to be copied. It uses consistently sized writes so a subsequent wider read doesn&#8217;t need to wait for the processor to reconcile overlapping stores.<\/p>\n<p>Write barriers are only one part of the interaction between generated code\nand the garbage collector. During a compacting collection, the GC needs to\nplan where surviving objects will move and then update references to them. To\ndo that efficiently, it records their addresses, sorts those addresses, and\ngroups adjacent survivors into regions called &#8220;plugs.&#8221; With enough live\nobjects, sorting these mark lists becomes a meaningful part of the collection.\nRecent x86\/x64 runtimes use a vectorized <code>vxsort<\/code> implementation for\nsufficiently large lists. In .NET 11,\n<a href=\"https:\/\/github.com\/dotnet\/runtime\/pull\/110692\">dotnet\/runtime#110692<\/a> from\n<a href=\"https:\/\/github.com\/a74nh\">@a74nh<\/a> extends that support to Arm64.<\/p>\n<p>The generation assigned to GC metadata matters just as much as the speed of\none collection. .NET&#8217;s generational GC is based on the observation that most\nobjects die young: generation 0 and generation 1 collections, collectively\ncalled ephemeral collections, run frequently and should avoid revisiting\nstate that has already survived into generation 2. A dependent handle\nassociates a primary object with a secondary object, keeping the secondary\nalive while the primary remains reachable; <code>ConditionalWeakTable&lt;TKey, TValue&gt;<\/code> is built on this mechanism. Previously, the handle itself didn&#8217;t age\nwith its referents, so every ephemeral collection continued scanning it even\nafter both objects had become long-lived.\n<a href=\"https:\/\/github.com\/dotnet\/runtime\/pull\/78746\">dotnet\/runtime#78746<\/a> ages\ndependent handles accordingly and moves a handle back to a younger generation\nwhen necessary. Old handles can therefore be skipped by young collections\nwithout compromising reachability.<\/p>\n<pre><code class=\"language-csharp\">\/\/ dotnet run -c Release -f net10.0 --filter \"*\" --runtimes net10.0 net11.0\r\n\r\nusing System.Runtime.CompilerServices;\r\nusing BenchmarkDotNet.Attributes;\r\nusing BenchmarkDotNet.Running;\r\n\r\nBenchmarkSwitcher.FromAssembly(typeof(Benchmarks).Assembly).Run(args);\r\n\r\n[HideColumns(\"Job\", \"Error\", \"StdDev\", \"Median\", \"RatioSD\")]\r\npublic class Benchmarks\r\n{\r\n    private ConditionalWeakTable&lt;object, object&gt; _table = new();\r\n    private object[] _keys = [];\r\n\r\n    [Params(100_000, 1_000_000)]\r\n    public int Handles { get; set; }\r\n\r\n    [GlobalSetup]\r\n    public void Setup()\r\n    {\r\n        _table = new();\r\n        _keys = new object[Handles];\r\n\r\n        for (int i = 0; i &lt; _keys.Length; i++)\r\n        {\r\n            object key = new();\r\n            _keys[i] = key;\r\n            _table.Add(key, new object());\r\n        }\r\n\r\n        GC.Collect(2, GCCollectionMode.Forced, blocking: true, compacting: true);\r\n    }\r\n\r\n    [Benchmark]\r\n    public void CollectGen0() =&gt;\r\n        GC.Collect(0, GCCollectionMode.Forced, blocking: true, compacting: false);\r\n}<\/code><\/pre>\n<table>\n<thead>\n<tr>\n<th>Method<\/th>\n<th>Runtime<\/th>\n<th>Handles<\/th>\n<th>Mean<\/th>\n<th>Ratio<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td>CollectGen0<\/td>\n<td>.NET 10.0<\/td>\n<td>100000<\/td>\n<td>1.522 ms<\/td>\n<td>1.00<\/td>\n<\/tr>\n<tr>\n<td>CollectGen0<\/td>\n<td>.NET 11.0<\/td>\n<td>100000<\/td>\n<td>255.5 \u03bcs<\/td>\n<td>0.17<\/td>\n<\/tr>\n<tr>\n<td>CollectGen0<\/td>\n<td>.NET 10.0<\/td>\n<td>1000000<\/td>\n<td>10.737 ms<\/td>\n<td>1.00<\/td>\n<\/tr>\n<tr>\n<td>CollectGen0<\/td>\n<td>.NET 11.0<\/td>\n<td>1000000<\/td>\n<td>310.7 \u03bcs<\/td>\n<td>0.029<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<h3>Runtime Knowledge and Frozen Data<\/h3>\n<p>The JIT can optimize only the facts it knows. Some facts come from its own analysis; others are contracts supplied by the runtime, such as which helpers have side effects, the length of a newly allocated string, or whether a data object will ever move.<\/p>\n<p>A generic virtual call such as <code>baseReference.Foo&lt;string&gt;()<\/code> may need help from the runtime to find the implementation for both the object&#8217;s actual type and the generic argument. If that lookup appears to have arbitrary side effects, the JIT has to perform it exactly where it occurs, rather than possibly resulting on a cached answer from a previous lookup. In .NET 11, <a href=\"https:\/\/github.com\/dotnet\/runtime\/pull\/122017\">dotnet\/runtime#122017<\/a> teaches the JIT more precisely which exceptions these runtime helpers can throw and whether they otherwise have side effects. The JIT can then share repeated lookups or move an unchanging lookup out of a loop:<\/p>\n<pre><code class=\"language-csharp\">\/\/ dotnet run -c Release -f net10.0 --filter \"*\" --runtimes net10.0 net11.0\r\n\r\nusing BenchmarkDotNet.Attributes;\r\nusing BenchmarkDotNet.Running;\r\nusing System.Runtime.CompilerServices;\r\n\r\nBenchmarkSwitcher.FromAssembly(typeof(Benchmarks).Assembly).Run(args);\r\n\r\n[HideColumns(\"Job\", \"Error\", \"StdDev\", \"Median\", \"RatioSD\")]\r\npublic class Benchmarks\r\n{\r\n    public abstract class Base\r\n    {\r\n        public abstract void Foo&lt;T&gt;();\r\n    }\r\n\r\n    public class Derived : Base\r\n    {\r\n        public override void Foo&lt;T&gt;() { }\r\n    }\r\n\r\n    private Base _b = new Derived();\r\n\r\n    [Benchmark]\r\n    public void GvmCseHoist()\r\n    {\r\n        Base b = _b;\r\n        b.Foo&lt;string&gt;();\r\n        b.Foo&lt;int&gt;();\r\n        b.Foo&lt;string&gt;();\r\n        b.Foo&lt;int&gt;();\r\n\r\n        for (int i = 0; i &lt; 10; i++)\r\n            b.Foo&lt;double&gt;();\r\n    }\r\n}<\/code><\/pre>\n<p>In .NET 11, the repeated lookups outside the loop are shared and the loop&#8217;s lookup is performed once, not ten times.<\/p>\n<table>\n<thead>\n<tr>\n<th>Method<\/th>\n<th>Runtime<\/th>\n<th>Mean<\/th>\n<th>Ratio<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td>GvmCseHoist<\/td>\n<td>.NET 10.0<\/td>\n<td>45.75 ns<\/td>\n<td>1.00<\/td>\n<\/tr>\n<tr>\n<td>GvmCseHoist<\/td>\n<td>.NET 11.0<\/td>\n<td>24.06 ns<\/td>\n<td>0.53<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<p>Profile data is another way the JIT learns what matters. Inlining could previously hide important work from the instrumentation used to gather that data. <a href=\"https:\/\/github.com\/dotnet\/runtime\/pull\/119658\">dotnet\/runtime#119658<\/a> allows the inlined code to be instrumented as well, giving later PGO-driven compilation a more complete picture of the hot paths.<\/p>\n<h3>JIT Throughput and Cleanup<\/h3>\n<p>The quality of the generated code isn&#8217;t the only concern; the time spent producing it matters too. Every analysis the JIT performs has a cost. <a href=\"https:\/\/github.com\/dotnet\/runtime\/pull\/123856\">dotnet\/runtime#123856<\/a> removes checks and maps from Global Assertion Propagation whose bookkeeping wasn&#8217;t paying for itself. This is the recurring balancing act in the development of the JIT: retaining the information that enables meaningful optimizations while avoiding analysis overhead whose code-quality benefit is negligible.<\/p>\n<p><a href=\"https:\/\/github.com\/dotnet\/runtime\/pull\/127363\">dotnet\/runtime#127363<\/a> makes profile-guided optimization more resilient with OSR (on-stack replacement), which replaces a method while one of its loops is already running. Because that execution begins in the middle of the method rather than at its normal entry, reconstructed profile data doesn&#8217;t always line up perfectly with the paths actually available. The JIT now estimates the likelihood of those paths rather than asserting or abandoning the profile.<\/p>\n<p>Optimizations can leave behind code that&#8217;s no longer reachable, so the JIT also needs to be good at dead code removal. <a href=\"https:\/\/github.com\/dotnet\/runtime\/pull\/126223\">dotnet\/runtime#126223<\/a> runs another sweep whenever the method&#8217;s branching structure changes, catching blocks made obsolete by earlier transformations.<\/p>\n<p>And <a href=\"https:\/\/github.com\/dotnet\/runtime\/pull\/128515\">dotnet\/runtime#128515<\/a> from <a href=\"https:\/\/github.com\/BoyBaykiller\">@BoyBaykiller<\/a> repeatedly combines equivalent return and throw endings, removing duplicate exit paths and sometimes exposing more code that can be shared.<\/p>\n<pre><code class=\"language-csharp\">\/\/ dotnet run -c Release -f net10.0 --filter \"*\" --runtimes net10.0 net11.0\r\n\r\nusing BenchmarkDotNet.Attributes;\r\nusing BenchmarkDotNet.Running;\r\n\r\nBenchmarkSwitcher.FromAssembly(typeof(Benchmarks).Assembly).Run(args);\r\n\r\n[DisassemblyDiagnoser, HideColumns(\"Job\", \"Error\", \"StdDev\", \"Median\", \"RatioSD\")]\r\npublic class Benchmarks\r\n{\r\n    [Benchmark]\r\n    [Arguments((byte)9)]\r\n    public bool IsLinearWhiteSpace(byte value) =&gt;\r\n        value &lt;= 32 &amp;&amp;\r\n        (value == 32 || value == 10 || value == 13 || value == 9);\r\n}<\/code><\/pre>\n<p>In .NET 10, tail merging combines the paths that return <code>false<\/code>, but not both paths that return <code>true<\/code>. As a result, the JIT&#8217;s bit test covers three of the four values, with a separate comparison for <code>9<\/code>. In .NET 11, the true returns are merged as well, enabling all four values to be handled by the same bit test:<\/p>\n<pre><code class=\"language-diff\">; x64\r\n-       movzx    ecx, dl\r\n-       cmp      ecx, 20\r\n-       jg       M00_L02\r\n-       cmp      ecx, 20\r\n-       ja       M00_L01\r\n-       mov      eax, 0FFFFDBFF\r\n-       bt       rax, rcx\r\n-       jae      M00_L00\r\n-       mov      eax, 1\r\n-       ret\r\n-M00_L00:\r\n-       cmp      ecx, 9\r\n-       sete     al\r\n-       movzx    eax, al\r\n-       ret\r\n-M00_L01:\r\n+       movzx    eax, dl\r\n+       cmp      eax, 20\r\n+       jg       M00_L00\r\n+       cmp      eax, 20\r\n+       ja       M00_L00\r\n+       mov      ecx, 0FFFFD9FF\r\n+       bt       rcx, rax\r\n+       jb       M00_L00\r\n+       mov      eax, 1\r\n+       ret\r\n+M00_L00:\r\n        xor      eax, eax\r\n        ret\r\n\r\n-; Total bytes of code 43\r\n+; Total bytes of code 33<\/code><\/pre>\n<p>Also related to dead code, a call that never returns, such as one that always throws, makes everything after it unreachable. In .NET 11, after inlining, <a href=\"https:\/\/github.com\/dotnet\/runtime\/pull\/128513\">dotnet\/runtime#128513<\/a> removes the remaining statements and outgoing paths from such a block and marks it as ending in a throw, exposing the dead code early enough for the cleanup passes above to remove it.<\/p>\n<h2>Startup and Deployment<\/h2>\n<p>Before managed <code>Main<\/code> can run, the native host needs to locate the application&#8217;s dependencies, CoreCLR needs to load enough types and code to begin execution, and various pieces of framework infrastructure need to initialize themselves. Work removed from any of those stages helps the application get going sooner, improving startup time.<\/p>\n<p>The host starts by reading the application&#8217;s <code>.deps.json<\/code>, turning its entries into paths, and building the trusted platform assembly (TPA) list. That list tells CoreCLR which framework and application assemblies it can resolve by simple name. Several costs in this process scaled with the number of assets rather than with the amount of useful work. <a href=\"https:\/\/github.com\/dotnet\/runtime\/pull\/123568\">dotnet\/runtime#123568<\/a> in .NET 11 avoids checking every asset against a servicing directory unless the resolver is actually probing that directory. <a href=\"https:\/\/github.com\/dotnet\/runtime\/pull\/123919\">dotnet\/runtime#123919<\/a> avoids repeatedly comparing the servicing-directory name and copying every dependency asset while constructing the TPA list, avoiding a lot of allocation. <a href=\"https:\/\/github.com\/dotnet\/runtime\/pull\/125251\">dotnet\/runtime#125251<\/a> removes more allocation by normalizing each asset&#8217;s directory separators once when parsing the <code>.deps.json<\/code>, rather than normalizing the path again every time it is used.<\/p>\n<p>Once the host hands off to CoreCLR, ReadyToRun (R2R) code helps avoid compiling methods before they can execute. However, initializing <code>Comparer&lt;T&gt;.Default<\/code> and <code>EqualityComparer&lt;T&gt;.Default<\/code> called a reflection-based helper whose resulting concrete comparer type wasn&#8217;t known when the R2R image was built. The comparer constructor and operations could consequently fall back to being interpreted. In .NET 11, <a href=\"https:\/\/github.com\/dotnet\/runtime\/pull\/126204\">dotnet\/runtime#126204<\/a> uses specialized helpers that R2R can compile ahead of time and ensures the required comparer types are included in the image.<\/p>\n<p>Even better than making initialization faster is avoiding it altogether. An <code>EventSource<\/code> normally discovers its event metadata and computes its provider GUID when it is initialized. <a href=\"https:\/\/github.com\/dotnet\/runtime\/pull\/121180\">dotnet\/runtime#121180<\/a> adds an internal source generator that performs this work when the framework is built and emits the result for its <code>EventSource<\/code> implementations, including the ones for core runtime tracing. Applications then don&#8217;t need to pay the reflection and setup costs when those event sources are first used.<\/p>\n<p>Startup also has a memory footprint outside the managed heap. Native AOT&#8217;s <code>AllocHeap<\/code> typically holds only small amounts of runtime metadata. On Windows, however, its virtual-memory allocator reserved a 64 KB region for each block even when it initially needed only 4 KB. In .NET 11, <a href=\"https:\/\/github.com\/dotnet\/runtime\/pull\/122822\">dotnet\/runtime#122822<\/a> instead uses ordinary <code>new<\/code> and <code>delete<\/code> for these small blocks, matching the allocation strategy to the amount of memory normally involved.<\/p>\n<p>Note that the aforementioned R2R work wasn&#8217;t motivated only by desktop and server startup. It was also\npart of the substantial effort to make CoreCLR the runtime for .NET on mobile.\nStarting with .NET 11, <a href=\"https:\/\/devblogs.microsoft.com\/dotnet\/dotnet-maui-moves-to-coreclr-in-dotnet-11\/\">.NET MAUI moved to CoreCLR<\/a>\nfor Android, iOS, and Mac Catalyst, the last .NET MAUI platforms that had still\nbeen using Mono. This is much more than swapping one execution engine for another. Those apps\nnow use the same runtime as ASP.NET Core, cloud services, and desktop .NET,\nwith the same JIT, garbage collector, diagnostics infrastructure, performance improvements, and bug fixes.\nIt also brings CoreCLR&#8217;s tiered compilation, ReadyToRun, and profile-guided\noptimization to mobile, while providing a common foundation for NativeAOT.\nThat combination is important: R2R and packaged profiles can precompile the\ncode most important to startup, while the optimizing JIT can produce\nhigher-quality code for hot methods on platforms where dynamic compilation is\navailable. Improvements like the comparer specialization mentioned earlier keep more code\non the compiled path instead of falling back to interpretation.<\/p>\n<h2>Threading<\/h2>\n<p>Threading is a cross-cutting concern that impacts almost every\napplication and service. Whether code is protecting shared state, queueing work, or\ncoordinating asynchronous operations, small costs in the underlying machinery\ncan quickly add up. As such, it&#8217;s something that&#8217;s revisited in every release of .NET.<\/p>\n<p><code>Monitor<\/code> is the synchronization primitive historically used to implement <code>lock<\/code>, providing the most pervasively used support for mutual exclusion. It also supports sending signals, such that one thread can wait on a <code>Monitor<\/code> with <code>Monitor.Wait<\/code> for another thread to <code>Pulse<\/code> it. The internal object that tracks these waiters is a &#8220;condition variable.&#8221; <a href=\"https:\/\/github.com\/dotnet\/runtime\/pull\/129083\">dotnet\/runtime#129083<\/a> stores that condition directly on the lock, removing a separate <code>ConditionalWeakTable<\/code> lookup from this already synchronization-heavy path.<\/p>\n<pre><code class=\"language-csharp\">\/\/ dotnet run -c Release -f net10.0 --filter \"*\" --runtimes net10.0 net11.0\r\n\r\nusing BenchmarkDotNet.Attributes;\r\nusing BenchmarkDotNet.Running;\r\n\r\nBenchmarkSwitcher.FromAssembly(typeof(Benchmarks).Assembly).Run(args);\r\n\r\n[HideColumns(\"Job\", \"Error\", \"StdDev\", \"Median\", \"RatioSD\")]\r\npublic class Benchmarks\r\n{\r\n    private const int RoundTripsPerInvoke = 2_000;\r\n\r\n    private readonly object _gate = new();\r\n    private int _ping;\r\n    private int _pong;\r\n    private bool _stop;\r\n    private Thread _responder = null!;\r\n\r\n    [GlobalSetup]\r\n    public void Setup()\r\n    {\r\n        _responder = new Thread(ResponderLoop) { IsBackground = true };\r\n        _responder.Start();\r\n    }\r\n\r\n    [GlobalCleanup]\r\n    public void Cleanup()\r\n    {\r\n        lock (_gate)\r\n        {\r\n            _stop = true;\r\n            Monitor.PulseAll(_gate);\r\n        }\r\n\r\n        _responder.Join();\r\n    }\r\n\r\n    private void ResponderLoop()\r\n    {\r\n        lock (_gate)\r\n        {\r\n            int seen = 0;\r\n            while (true)\r\n            {\r\n                while (_ping == seen &amp;&amp; !_stop)\r\n                    Monitor.Wait(_gate);\r\n\r\n                if (_stop)\r\n                    return;\r\n\r\n                seen = _ping;\r\n                _pong = seen;\r\n                Monitor.PulseAll(_gate);\r\n            }\r\n        }\r\n    }\r\n\r\n    [Benchmark(OperationsPerInvoke = RoundTripsPerInvoke)]\r\n    public int PingPong_MonitorWaitPulse()\r\n    {\r\n        lock (_gate)\r\n        {\r\n            for (int i = 0; i &lt; RoundTripsPerInvoke; i++)\r\n            {\r\n                _ping++;\r\n                int expected = _ping;\r\n                Monitor.PulseAll(_gate);\r\n                while (_pong != expected)\r\n                    Monitor.Wait(_gate);\r\n            }\r\n\r\n            return _pong;\r\n        }\r\n    }\r\n}<\/code><\/pre>\n<table>\n<thead>\n<tr>\n<th>Method<\/th>\n<th>Runtime<\/th>\n<th>Mean<\/th>\n<th>Ratio<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td>PingPong_MonitorWaitPulse<\/td>\n<td>.NET 10.0<\/td>\n<td>4.194 \u03bcs<\/td>\n<td>1.00<\/td>\n<\/tr>\n<tr>\n<td>PingPong_MonitorWaitPulse<\/td>\n<td>.NET 11.0<\/td>\n<td>3.517 \u03bcs<\/td>\n<td>0.84<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<p>In the case of <code>Monitor<\/code>, that improvement targeted the specific shared implementation. In other cases, the costs are spread out in a more peanut butter manner across lots of code. <a href=\"https:\/\/github.com\/dotnet\/runtime\/pull\/125274\">dotnet\/runtime#125274<\/a> removes some of that peanut butter by removing unnecessary <code>volatile<\/code> annotations from a wide range of library fields whose correctness already comes from locks, <code>Interlocked<\/code>, or one-time initialization. On x86\/x64 hardware, which already provides a strong memory model, those annotations generally don&#8217;t result in extra instructions, though they can still constrain compiler optimizations. Arm, however, permits more reordering, so the JIT often needs to emit memory fences to provide <code>volatile<\/code>&#8216;s guarantees. Removing the annotations where they&#8217;re redundant therefore can end up removing unnecessary fences from Arm&#8217;s generated code.<\/p>\n<p>Similar considerations apply to code in the runtime. <a href=\"https:\/\/github.com\/dotnet\/runtime\/pull\/125259\">dotnet\/runtime#125259<\/a> replaces\nfull memory barriers in the runtime&#8217;s <code>HashMap<\/code> with the narrower acquire and\nrelease operations actually required. On top of that, many VM\nhash tables, including its <code>EEHashTable<\/code>, are read constantly but updated only\noccasionally. <a href=\"https:\/\/github.com\/dotnet\/runtime\/pull\/124822\">dotnet\/runtime#124822<\/a>\nadds epoch-based reclamation, enabling readers to avoid entering cooperative\nGC mode simply to keep an old set of buckets alive. And <a href=\"https:\/\/github.com\/dotnet\/runtime\/pull\/129640\">dotnet\/runtime#129640<\/a> replaces\nthe previous byte-at-a-time hash used by these tables with an xxHash\nimplementation that consumes four bytes at a time.<\/p>\n<p>Along the same lines, in .NET 11 <a href=\"https:\/\/github.com\/dotnet\/runtime\/pull\/122726\">dotnet\/runtime#122726<\/a> reduces the scheduling overhead around small thread-pool work items. It removes unnecessary memory fences and shared-state updates, checks in with the thread-pool controller once per batch rather than once per item, spends less time spinning on a semaphore, and requests another worker only when the queued work shows one is needed. The result is less coordination overhead and fewer workers woken just as the queue becomes empty.<\/p>\n<p>Earlier in this post, we talked about runtime async, which can have a significant impact on the performance of <code>async<\/code>\/<code>await<\/code> code, how they produce <code>Task<\/code>s, and so on. They&#8217;re not the only improvements in .NET 11 related to <code>Task<\/code>s, though.<\/p>\n<p>One fun one is a new analyzer, CA2027, introduced in <a href=\"https:\/\/github.com\/dotnet\/sdk\/pull\/51452\">dotnet\/sdk#51452<\/a>. With that, the SDK can point out problematic usage of <code>Task.Delay<\/code> that I&#8217;ve seen on multiple occasions to lead to non-trivial performance issues in large scale services. Consider this code:<\/p>\n<pre><code class=\"language-csharp\">Task someTask = ...;\r\nif (await Task.WhenAny(someTask, Task.Delay(timeout)) != someTask) \/\/ oops!\r\n{\r\n    throw new TimeoutException();\r\n}<\/code><\/pre>\n<p>The developer that wrote this is obviously trying to implement a timeout. The problem, however, is that this leaks. In the hopefully common case where <code>someTask<\/code> completes really quickly, the <code>Task.Delay<\/code> will still be pending. That <code>Delay<\/code> has associated with it a <code>System.Threading.Timer<\/code> that&#8217;s consuming valuable resources, as well as other data in memory, and if this <code>timeout<\/code> is long and this code is on a hotter path, we could accumulate thousands upon thousands of those timers. That in turn can increase memory use and slow down other calls that interact with timers.<\/p>\n<p>The fix is to instead use the <code>Task.WaitAsync<\/code> method, introduced all the way back in .NET 6. It provides a much more efficient mechanism for doing this same kind of timed waiting, and it correctly handles all the relevant cleanup. CA2027 will detect common forms of this issue and recommend the replacement.<\/p>\n<h2>Numerics<\/h2>\n<p><code>BigInteger<\/code> is one of those types that many applications may never need, but\nfor those that do, there&#8217;s often no practical substitute. It powers workloads\nranging from cryptography and number theory to compilers and applications that\nneed to parse, format, or compute with integers larger than the fixed-width\nprimitives can hold. Despite that need, however, <code>BigInteger<\/code> hasn&#8217;t received the\nsame steady stream of performance investment as many of .NET&#8217;s other core\ntypes. Thankfully, in .NET 11 it gets a makeover.<\/p>\n<p><a href=\"https:\/\/github.com\/dotnet\/runtime\/pull\/125799\">dotnet\/runtime#125799<\/a> rewrote significant portions of <code>BigInteger<\/code>&#8216;s implementation, changing its limbs (the fixed-size pieces stored in its backing array) from <code>uint<\/code> to <code>nuint<\/code> (<code>UIntPtr<\/code>). That makes no effective difference on a 32-bit machine. On a 64-bit machine, however, each limb grows from 32 to 64 bits; since most arithmetic on a 64-bit value on a 64-bit platform costs no more than the corresponding 32-bit operation, each step can therefore process twice as many bits in the same number of cycles. The implementation also improves the algorithms around those wider limbs, including Montgomery multiplication and sliding-window exponentiation in <code>ModPow<\/code>, fused bitwise steps, additional hardware intrinsics, loop unrolling, and caching. That all builds on top of other optimizations that were done previously in the release, such as faster conversion of huge values to decimal text in <a href=\"https:\/\/github.com\/dotnet\/runtime\/pull\/112178\">dotnet\/runtime#112178<\/a> from <a href=\"https:\/\/github.com\/kzrnm\">@kzrnm<\/a>, <a href=\"https:\/\/github.com\/dotnet\/runtime\/pull\/112876\">dotnet\/runtime#112876<\/a> from <a href=\"https:\/\/github.com\/kzrnm\">@kzrnm<\/a> using Toom-Cook multiplication for sufficiently large operands, and improved shifts and rotations thanks to <a href=\"https:\/\/github.com\/dotnet\/runtime\/pull\/113005\">dotnet\/runtime#113005<\/a> from <a href=\"https:\/\/github.com\/kzrnm\">@kzrnm<\/a>. Toom-Cook splits each operand into several chunks and combines smaller products, doing less work than the straightforward every-limb-by-every-limb algorithm once the operands are large enough.<\/p>\n<pre><code class=\"language-csharp\">\/\/ dotnet run -c Release -f net10.0 --filter \"*\" --runtimes net10.0 net11.0\r\n\r\nusing BenchmarkDotNet.Attributes;\r\nusing BenchmarkDotNet.Running;\r\nusing System.Numerics;\r\nusing System.Globalization;\r\nusing System.Text;\r\n\r\nBenchmarkSwitcher.FromAssembly(typeof(Benchmarks).Assembly).Run(args);\r\n\r\n[HideColumns(\"Job\", \"Error\", \"StdDev\", \"Median\", \"RatioSD\")]\r\npublic class Benchmarks\r\n{\r\n    [Params(64, 512)]\r\n    public int Limbs;\r\n\r\n    private BigInteger _a;\r\n    private BigInteger _b;\r\n    private BigInteger _shiftSubject;\r\n    private BigInteger _hugeValueForToString;\r\n    private string _decimalDigits100000 = \"\";\r\n    private byte[] _utf8Digits1000 = [];\r\n    private byte[] _utf8FormatBuffer = new byte[120_000];\r\n\r\n    private BigInteger _divideDividendBelowThreshold;\r\n    private BigInteger _divideDivisorBelowThreshold;\r\n    private BigInteger _divideDividendAboveThreshold;\r\n    private BigInteger _divideDivisorAboveThreshold;\r\n\r\n    [GlobalSetup]\r\n    public void Setup()\r\n    {\r\n        _a = MakeDeterministicBigInteger(Limbs, seed: 1);\r\n        _b = MakeDeterministicBigInteger(Limbs, seed: 2);\r\n        _shiftSubject = MakeDeterministicBigInteger(Limbs, seed: 3);\r\n\r\n        _decimalDigits100000 = MakeDeterministicDecimalDigits(100_000);\r\n        _hugeValueForToString = BigInteger.Parse(_decimalDigits100000, CultureInfo.InvariantCulture);\r\n\r\n        string decimalDigits1000 = MakeDeterministicDecimalDigits(1_000);\r\n        _utf8Digits1000 = Encoding.UTF8.GetBytes(decimalDigits1000);\r\n\r\n        _divideDivisorBelowThreshold = MakeDeterministicBigInteger(16, seed: 4);\r\n        _divideDividendBelowThreshold = MakeDeterministicBigInteger(16 + 96, seed: 5);\r\n\r\n        _divideDivisorAboveThreshold = MakeDeterministicBigInteger(128, seed: 6);\r\n        _divideDividendAboveThreshold = MakeDeterministicBigInteger(128 + 96, seed: 7);\r\n    }\r\n\r\n    private static BigInteger MakeDeterministicBigInteger(int limbCount, int seed)\r\n    {\r\n        Random rng = new(seed);\r\n        byte[] bytes = new byte[(limbCount * 4) + 1]; \/\/ trailing 0 byte keeps the value positive\r\n        rng.NextBytes(bytes);\r\n        bytes[^1] = 0;\r\n        return new BigInteger(bytes);\r\n    }\r\n\r\n    private static string MakeDeterministicDecimalDigits(int digitCount)\r\n    {\r\n        StringBuilder sb = new(digitCount);\r\n        sb.Append('9'); \/\/ avoid a leading zero, which would shorten the effective digit count\r\n        Random rng = new(42);\r\n        for (int i = 1; i &lt; digitCount; i++)\r\n            sb.Append((char)('0' + rng.Next(0, 10)));\r\n\r\n        return sb.ToString();\r\n    }\r\n\r\n    [Benchmark]\r\n    public BigInteger Divide_BelowBurnikelZieglerThreshold() =&gt; _divideDividendBelowThreshold \/ _divideDivisorBelowThreshold;\r\n\r\n    [Benchmark]\r\n    public BigInteger Divide_AboveBurnikelZieglerThreshold() =&gt; _divideDividendAboveThreshold \/ _divideDivisorAboveThreshold;\r\n\r\n    [Benchmark]\r\n    public BigInteger Multiply() =&gt; _a * _b;\r\n\r\n    [Benchmark]\r\n    public BigInteger ShiftLeft() =&gt; _shiftSubject &lt;&lt; 12345;\r\n\r\n    [Benchmark]\r\n    public BigInteger ParseLargeDecimal() =&gt; BigInteger.Parse(_decimalDigits100000, CultureInfo.InvariantCulture);\r\n\r\n    [Benchmark]\r\n    public string ToStringLargeDecimal() =&gt; _hugeValueForToString.ToString(CultureInfo.InvariantCulture);\r\n}<\/code><\/pre>\n<table>\n<thead>\n<tr>\n<th>Method<\/th>\n<th>Runtime<\/th>\n<th>Limbs<\/th>\n<th>Mean<\/th>\n<th>Ratio<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td>Divide_BelowBurnikelZieglerThreshold<\/td>\n<td>.NET 10.0<\/td>\n<td>64<\/td>\n<td>2,954.3 ns<\/td>\n<td>1.00<\/td>\n<\/tr>\n<tr>\n<td>Divide_BelowBurnikelZieglerThreshold<\/td>\n<td>.NET 11.0<\/td>\n<td>64<\/td>\n<td>1,493.8 ns<\/td>\n<td>0.51<\/td>\n<\/tr>\n<tr>\n<td>Divide_AboveBurnikelZieglerThreshold<\/td>\n<td>.NET 10.0<\/td>\n<td>64<\/td>\n<td>10,766.7 ns<\/td>\n<td>1.00<\/td>\n<\/tr>\n<tr>\n<td>Divide_AboveBurnikelZieglerThreshold<\/td>\n<td>.NET 11.0<\/td>\n<td>64<\/td>\n<td>6,303.4 ns<\/td>\n<td>0.59<\/td>\n<\/tr>\n<tr>\n<td>Multiply<\/td>\n<td>.NET 10.0<\/td>\n<td>64<\/td>\n<td>2,359.5 ns<\/td>\n<td>1.00<\/td>\n<\/tr>\n<tr>\n<td>Multiply<\/td>\n<td>.NET 11.0<\/td>\n<td>64<\/td>\n<td>1,328.2 ns<\/td>\n<td>0.56<\/td>\n<\/tr>\n<tr>\n<td>ShiftLeft<\/td>\n<td>.NET 10.0<\/td>\n<td>64<\/td>\n<td>217.1 ns<\/td>\n<td>1.00<\/td>\n<\/tr>\n<tr>\n<td>ShiftLeft<\/td>\n<td>.NET 11.0<\/td>\n<td>64<\/td>\n<td>121.6 ns<\/td>\n<td>0.56<\/td>\n<\/tr>\n<tr>\n<td>ParseLargeDecimal<\/td>\n<td>.NET 10.0<\/td>\n<td>64<\/td>\n<td>9,111,926.1 ns<\/td>\n<td>1.00<\/td>\n<\/tr>\n<tr>\n<td>ParseLargeDecimal<\/td>\n<td>.NET 11.0<\/td>\n<td>64<\/td>\n<td>3,899,478.0 ns<\/td>\n<td>0.43<\/td>\n<\/tr>\n<tr>\n<td>ToStringLargeDecimal<\/td>\n<td>.NET 10.0<\/td>\n<td>64<\/td>\n<td>135,894,135.4 ns<\/td>\n<td>1.00<\/td>\n<\/tr>\n<tr>\n<td>ToStringLargeDecimal<\/td>\n<td>.NET 11.0<\/td>\n<td>64<\/td>\n<td>7,868,359.3 ns<\/td>\n<td>0.058<\/td>\n<\/tr>\n<tr>\n<td>Divide_BelowBurnikelZieglerThreshold<\/td>\n<td>.NET 10.0<\/td>\n<td>512<\/td>\n<td>2,894.6 ns<\/td>\n<td>1.00<\/td>\n<\/tr>\n<tr>\n<td>Divide_BelowBurnikelZieglerThreshold<\/td>\n<td>.NET 11.0<\/td>\n<td>512<\/td>\n<td>1,482.0 ns<\/td>\n<td>0.51<\/td>\n<\/tr>\n<tr>\n<td>Divide_AboveBurnikelZieglerThreshold<\/td>\n<td>.NET 10.0<\/td>\n<td>512<\/td>\n<td>10,751.7 ns<\/td>\n<td>1.00<\/td>\n<\/tr>\n<tr>\n<td>Divide_AboveBurnikelZieglerThreshold<\/td>\n<td>.NET 11.0<\/td>\n<td>512<\/td>\n<td>6,317.8 ns<\/td>\n<td>0.59<\/td>\n<\/tr>\n<tr>\n<td>Multiply<\/td>\n<td>.NET 10.0<\/td>\n<td>512<\/td>\n<td>68,063.6 ns<\/td>\n<td>1.00<\/td>\n<\/tr>\n<tr>\n<td>Multiply<\/td>\n<td>.NET 11.0<\/td>\n<td>512<\/td>\n<td>35,443.5 ns<\/td>\n<td>0.52<\/td>\n<\/tr>\n<tr>\n<td>ShiftLeft<\/td>\n<td>.NET 10.0<\/td>\n<td>512<\/td>\n<td>673.0 ns<\/td>\n<td>1.00<\/td>\n<\/tr>\n<tr>\n<td>ShiftLeft<\/td>\n<td>.NET 11.0<\/td>\n<td>512<\/td>\n<td>309.4 ns<\/td>\n<td>0.46<\/td>\n<\/tr>\n<tr>\n<td>ParseLargeDecimal<\/td>\n<td>.NET 10.0<\/td>\n<td>512<\/td>\n<td>9,149,793.0 ns<\/td>\n<td>1.00<\/td>\n<\/tr>\n<tr>\n<td>ParseLargeDecimal<\/td>\n<td>.NET 11.0<\/td>\n<td>512<\/td>\n<td>3,891,313.0 ns<\/td>\n<td>0.43<\/td>\n<\/tr>\n<tr>\n<td>ToStringLargeDecimal<\/td>\n<td>.NET 10.0<\/td>\n<td>512<\/td>\n<td>135,829,594.6 ns<\/td>\n<td>1.00<\/td>\n<\/tr>\n<tr>\n<td>ToStringLargeDecimal<\/td>\n<td>.NET 11.0<\/td>\n<td>512<\/td>\n<td>7,880,631.1 ns<\/td>\n<td>0.058<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<p>In addition to internal changes, <code>BigInteger<\/code> also gained new public APIs that avoid transcoding. Protocols and storage formats increasingly expose text as UTF-8 bytes, but the previous parsing and formatting APIs required UTF-16 characters. Callers therefore had to decode the input into a temporary string before parsing, or format into characters and encode the result back to bytes. <a href=\"https:\/\/github.com\/dotnet\/runtime\/pull\/117745\">dotnet\/runtime#117745<\/a> adds direct UTF-8 parsing and formatting to both <code>BigInteger<\/code> and <code>Complex<\/code>, sharing the generic numeric machinery used for UTF-16 and letting those consumers operate on their original representation.<\/p>\n<p><a href=\"https:\/\/github.com\/dotnet\/runtime\/pull\/130721\">dotnet\/runtime#130721<\/a> improves a different <code>BigInteger<\/code> boundary: casting to <code>double<\/code> and <code>float<\/code>. The general conversion needs to inspect the arbitrary-width magnitude, locate its highest set bits, and perform the rounding required by the target floating-point format. But many <code>BigInteger<\/code> instances are much smaller than that machinery is designed for&#8230; the implementation now recognizes values that fit in 64 bits and routes them through the hardware&#8217;s native integer conversion support.<\/p>\n<pre><code class=\"language-csharp\">\/\/ dotnet run -c Release -f net10.0 --filter \"*\" --runtimes net10.0 net11.0\r\n\r\nusing BenchmarkDotNet.Attributes;\r\nusing BenchmarkDotNet.Running;\r\nusing System.Numerics;\r\n\r\nBenchmarkSwitcher.FromAssembly(typeof(Benchmarks).Assembly).Run(args);\r\n\r\n[HideColumns(\"Job\", \"Error\", \"StdDev\", \"Median\", \"RatioSD\")]\r\npublic class Benchmarks\r\n{\r\n    private readonly BigInteger _small = (BigInteger.One &lt;&lt; 63) + 123;\r\n    private readonly BigInteger _large = (BigInteger.One &lt;&lt; 1023) + (BigInteger.One &lt;&lt; 511) + 123;\r\n\r\n    [Benchmark] public double SmallToDouble() =&gt; (double)_small;\r\n    [Benchmark] public float SmallToSingle() =&gt; (float)_small;\r\n    [Benchmark] public double LargeToDouble() =&gt; (double)_large;\r\n    [Benchmark] public float LargeToSingle() =&gt; (float)_large;\r\n}<\/code><\/pre>\n<table>\n<thead>\n<tr>\n<th>Method<\/th>\n<th>Runtime<\/th>\n<th>Mean<\/th>\n<th>Ratio<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td>SmallToDouble<\/td>\n<td>.NET 10.0<\/td>\n<td>2.873 ns<\/td>\n<td>1.00<\/td>\n<\/tr>\n<tr>\n<td>SmallToDouble<\/td>\n<td>.NET 11.0<\/td>\n<td>1.764 ns<\/td>\n<td>0.61<\/td>\n<\/tr>\n<tr>\n<td>SmallToSingle<\/td>\n<td>.NET 10.0<\/td>\n<td>3.548 ns<\/td>\n<td>1.00<\/td>\n<\/tr>\n<tr>\n<td>SmallToSingle<\/td>\n<td>.NET 11.0<\/td>\n<td>1.764 ns<\/td>\n<td>0.50<\/td>\n<\/tr>\n<tr>\n<td>LargeToDouble<\/td>\n<td>.NET 10.0<\/td>\n<td>2.863 ns<\/td>\n<td>1.00<\/td>\n<\/tr>\n<tr>\n<td>LargeToDouble<\/td>\n<td>.NET 11.0<\/td>\n<td>2.797 ns<\/td>\n<td>0.98<\/td>\n<\/tr>\n<tr>\n<td>LargeToSingle<\/td>\n<td>.NET 10.0<\/td>\n<td>3.559 ns<\/td>\n<td>1.00<\/td>\n<\/tr>\n<tr>\n<td>LargeToSingle<\/td>\n<td>.NET 11.0<\/td>\n<td>2.849 ns<\/td>\n<td>0.80<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<p>The same limb-widening advantages given to <code>BigInteger<\/code> in .NET 11 were also extended to the core floating-point types. Parsing a very long decimal input and formatting a floating-point value with many requested digits both need temporary arbitrary-precision arithmetic once the value no longer fits in the normal mantissa. .NET uses a separate internal <code>Number.BigInteger<\/code> for that work. <a href=\"https:\/\/github.com\/dotnet\/runtime\/pull\/132577\">dotnet\/runtime#132577<\/a> applies the same native-width limb representation to that type, reducing the amount of per-limb work in floating-point parsing, formatting, and rounding.<\/p>\n<pre><code class=\"language-csharp\">\/\/ dotnet run -c Release -f net10.0 --filter \"*\" --runtimes net10.0 net11.0\r\n\r\nusing System.Globalization;\r\nusing BenchmarkDotNet.Attributes;\r\nusing BenchmarkDotNet.Running;\r\n\r\nBenchmarkSwitcher.FromAssembly(typeof(Benchmarks).Assembly).Run(args);\r\n\r\n[HideColumns(\"Job\", \"Error\", \"StdDev\", \"Median\", \"RatioSD\")]\r\npublic class Benchmarks\r\n{\r\n    private readonly string _longFraction = \"0.\" + new string('1', 768);\r\n\r\n    [Benchmark]\r\n    public double ParseLongFraction() =&gt; double.Parse(_longFraction, CultureInfo.InvariantCulture);\r\n\r\n    [Benchmark]\r\n    public string FormatSubnormal() =&gt; double.Epsilon.ToString(\"G99\", CultureInfo.InvariantCulture);\r\n}<\/code><\/pre>\n<table>\n<thead>\n<tr>\n<th>Method<\/th>\n<th>Runtime<\/th>\n<th>Mean<\/th>\n<th>Ratio<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td>ParseLongFraction<\/td>\n<td>.NET 10.0<\/td>\n<td>8.592 \u03bcs<\/td>\n<td>1.00<\/td>\n<\/tr>\n<tr>\n<td>ParseLongFraction<\/td>\n<td>.NET 11.0<\/td>\n<td>3.569 \u03bcs<\/td>\n<td>0.42<\/td>\n<\/tr>\n<tr>\n<td>FormatSubnormal<\/td>\n<td>.NET 10.0<\/td>\n<td>6.884 \u03bcs<\/td>\n<td>1.00<\/td>\n<\/tr>\n<tr>\n<td>FormatSubnormal<\/td>\n<td>.NET 11.0<\/td>\n<td>1.237 \u03bcs<\/td>\n<td>0.18<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<p>The .NET 11 improvements aren&#8217;t limited to the scalar representations underlying\n<code>BigInteger<\/code> and floating-point parsing and formatting. Other numerical types improve as well. Consider <code>Matrix4x4<\/code>. A 4&#215;4 matrix\ndeterminant combines products of many independent matrix elements, making it a\nnatural fit for SIMD. <a href=\"https:\/\/github.com\/dotnet\/runtime\/pull\/123954\">dotnet\/runtime#123954<\/a>\nfrom <a href=\"https:\/\/github.com\/alexcovington\">@alexcovington<\/a> adds an SSE\nimplementation of <code>Matrix4x4.GetDeterminant<\/code>, evaluating several of those\nproducts in parallel:<\/p>\n<pre><code class=\"language-csharp\">\/\/ dotnet run -c Release -f net10.0 --filter \"*\" --runtimes net10.0 net11.0\r\n\r\nusing BenchmarkDotNet.Attributes;\r\nusing BenchmarkDotNet.Running;\r\nusing System.Numerics;\r\n\r\nBenchmarkSwitcher.FromAssembly(typeof(Benchmarks).Assembly).Run(args);\r\n\r\n[HideColumns(\"Job\", \"Error\", \"StdDev\", \"Median\", \"RatioSD\")]\r\npublic class Benchmarks\r\n{\r\n    private readonly Matrix4x4 _matrix =\r\n        Matrix4x4.CreateFromYawPitchRoll(0.4f, 0.8f, 1.1f) *\r\n        Matrix4x4.CreateTranslation(1.5f, -2.5f, 3.25f) *\r\n        Matrix4x4.CreateScale(1.1f, 0.9f, 1.05f);\r\n\r\n    [Benchmark]\r\n    public float GetDeterminant() =&gt; _matrix.GetDeterminant();\r\n}<\/code><\/pre>\n<table>\n<thead>\n<tr>\n<th>Method<\/th>\n<th>Runtime<\/th>\n<th>Mean<\/th>\n<th>Ratio<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td>GetDeterminant<\/td>\n<td>.NET 10.0<\/td>\n<td>3.836 ns<\/td>\n<td>1.00<\/td>\n<\/tr>\n<tr>\n<td>GetDeterminant<\/td>\n<td>.NET 11.0<\/td>\n<td>2.645 ns<\/td>\n<td>0.69<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<p>The <code>System.Numerics.Tensors<\/code> APIs are designed to perform the same numerical operation over many values, making them a natural fit for SIMD. <a href=\"https:\/\/github.com\/dotnet\/runtime\/pull\/126052\">dotnet\/runtime#126052<\/a> adds vector implementations of inverse sine to the portable vector types and uses them in <code>TensorPrimitives.Asin<\/code>. The tensor loop now evaluates a polynomial approximation for several inputs together, with special handling near the ends of the function&#8217;s <code>[-1, 1]<\/code> domain, rather than calling <code>MathF.Asin<\/code> or <code>Math.Asin<\/code> separately for every element:<\/p>\n<pre><code class=\"language-csharp\">\/\/ Run separately so each target uses its matching System.Numerics.Tensors package:\r\n\/\/ dotnet run -c Release -f net10.0 --filter \"*\"\r\n\/\/ dotnet run -c Release -f net11.0 --filter \"*\"\r\n\r\nusing BenchmarkDotNet.Attributes;\r\nusing BenchmarkDotNet.Running;\r\nusing System.Numerics.Tensors;\r\n\r\nBenchmarkSwitcher.FromAssembly(typeof(Benchmarks).Assembly).Run(args);\r\n\r\n[HideColumns(\"Job\", \"Error\", \"StdDev\", \"Median\", \"RatioSD\")]\r\npublic class Benchmarks\r\n{\r\n    private const int Length = 4096;\r\n\r\n    private float[] _floatsIn = new float[Length];\r\n    private float[] _floatsOut = new float[Length];\r\n    private double[] _doublesIn = new double[Length];\r\n    private double[] _doublesOut = new double[Length];\r\n\r\n    [GlobalSetup]\r\n    public void Setup()\r\n    {\r\n        Random rng = new(42);\r\n        for (int i = 0; i &lt; Length; i++)\r\n        {\r\n            float v = (float)((rng.NextDouble() * 2.0) - 1.0); \/\/ Asin's domain is [-1, 1]\r\n            _floatsIn[i] = v;\r\n            _doublesIn[i] = v;\r\n        }\r\n    }\r\n\r\n    [Benchmark]\r\n    public float AsinFloat()\r\n    {\r\n        TensorPrimitives.Asin(_floatsIn, _floatsOut);\r\n        return _floatsOut[0];\r\n    }\r\n\r\n    [Benchmark]\r\n    public double AsinDouble()\r\n    {\r\n        TensorPrimitives.Asin(_doublesIn, _doublesOut);\r\n        return _doublesOut[0];\r\n    }\r\n}<\/code><\/pre>\n<table>\n<thead>\n<tr>\n<th>Method<\/th>\n<th>Runtime<\/th>\n<th>Mean<\/th>\n<th>Ratio<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td>AsinFloat<\/td>\n<td>.NET 10.0<\/td>\n<td>33.72 \u03bcs<\/td>\n<td>1.00<\/td>\n<\/tr>\n<tr>\n<td>AsinFloat<\/td>\n<td>.NET 11.0<\/td>\n<td>8.240 \u03bcs<\/td>\n<td>0.24<\/td>\n<\/tr>\n<tr>\n<td>AsinDouble<\/td>\n<td>.NET 10.0<\/td>\n<td>35.80 \u03bcs<\/td>\n<td>1.00<\/td>\n<\/tr>\n<tr>\n<td>AsinDouble<\/td>\n<td>.NET 11.0<\/td>\n<td>10.975 \u03bcs<\/td>\n<td>0.31<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<p><code>TensorPrimitives<\/code> also picked up a few more targeted SIMD improvements. For floating-point values, <code>BitIncrement<\/code> and <code>BitDecrement<\/code> move to the immediately adjacent representable value; despite their names, they can&#8217;t simply add or subtract one, as they also need to handle signed zero, infinities, and NaNs correctly. <a href=\"https:\/\/github.com\/dotnet\/runtime\/pull\/123610\">dotnet\/runtime#123610<\/a> and <a href=\"https:\/\/github.com\/dotnet\/runtime\/pull\/123754\">dotnet\/runtime#123754<\/a> process multiple <code>float<\/code>\/<code>double<\/code> and <code>Half<\/code> values at once, respectively. The <code>Half<\/code> path works directly with the raw <code>ushort<\/code> bit patterns, avoiding conversion to <code>float<\/code> and back, and both paths use vector masks and conditional selection rather than calling a scalar helper for every element.<\/p>\n<pre><code class=\"language-csharp\">\/\/ Run separately so each target uses its matching System.Numerics.Tensors package:\r\n\/\/ dotnet run -c Release -f net10.0 --filter \"*\"\r\n\/\/ dotnet run -c Release -f net11.0 --filter \"*\"\r\n\r\nusing System.Numerics.Tensors;\r\nusing BenchmarkDotNet.Attributes;\r\nusing BenchmarkDotNet.Running;\r\n\r\nBenchmarkSwitcher.FromAssembly(typeof(Benchmarks).Assembly).Run(args);\r\n\r\n[HideColumns(\"Job\", \"Error\", \"StdDev\", \"Median\", \"RatioSD\")]\r\npublic class Benchmarks\r\n{\r\n    private const int Length = 4096;\r\n    private readonly float[] _floats = new float[Length];\r\n    private readonly float[] _floatDestination = new float[Length];\r\n    private readonly double[] _doubles = new double[Length];\r\n    private readonly double[] _doubleDestination = new double[Length];\r\n    private readonly Half[] _halves = new Half[Length];\r\n    private readonly Half[] _halfDestination = new Half[Length];\r\n\r\n    [GlobalSetup]\r\n    public void Setup()\r\n    {\r\n        for (int i = 0; i &lt; Length; i++)\r\n        {\r\n            float value = (i &amp; 7) switch\r\n            {\r\n                0 =&gt; 0,\r\n                1 =&gt; -0.0f,\r\n                2 =&gt; float.PositiveInfinity,\r\n                3 =&gt; float.NegativeInfinity,\r\n                4 =&gt; float.NaN,\r\n                _ =&gt; i \/ 7.0f,\r\n            };\r\n            _floats[i] = value;\r\n            _doubles[i] = value;\r\n            _halves[i] = (Half)value;\r\n        }\r\n    }\r\n\r\n    [Benchmark]\r\n    public void BitIncrementFloat() =&gt; TensorPrimitives.BitIncrement(_floats, _floatDestination);\r\n\r\n    [Benchmark]\r\n    public void BitIncrementDouble() =&gt; TensorPrimitives.BitIncrement(_doubles, _doubleDestination);\r\n\r\n    [Benchmark]\r\n    public void BitIncrementHalf() =&gt; TensorPrimitives.BitIncrement(_halves, _halfDestination);\r\n}<\/code><\/pre>\n<table>\n<thead>\n<tr>\n<th>Method<\/th>\n<th>Runtime<\/th>\n<th>Mean<\/th>\n<th>Ratio<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td>BitIncrementFloat<\/td>\n<td>.NET 10.0<\/td>\n<td>4.223 \u03bcs<\/td>\n<td>1.00<\/td>\n<\/tr>\n<tr>\n<td>BitIncrementFloat<\/td>\n<td>.NET 11.0<\/td>\n<td>1,071.1 ns<\/td>\n<td>0.25<\/td>\n<\/tr>\n<tr>\n<td>BitIncrementDouble<\/td>\n<td>.NET 10.0<\/td>\n<td>4.223 \u03bcs<\/td>\n<td>1.00<\/td>\n<\/tr>\n<tr>\n<td>BitIncrementDouble<\/td>\n<td>.NET 11.0<\/td>\n<td>2,140.6 ns<\/td>\n<td>0.51<\/td>\n<\/tr>\n<tr>\n<td>BitIncrementHalf<\/td>\n<td>.NET 10.0<\/td>\n<td>3.918 \u03bcs<\/td>\n<td>1.00<\/td>\n<\/tr>\n<tr>\n<td>BitIncrementHalf<\/td>\n<td>.NET 11.0<\/td>\n<td>573.6 ns<\/td>\n<td>0.15<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<p><a href=\"https:\/\/github.com\/dotnet\/runtime\/pull\/124280\">dotnet\/runtime#124280<\/a> removes a more mechanical cost from <code>TensorPrimitives.Round<\/code>: for <code>digits == 0<\/code>, the old code invoked a full-span rounding kernel and then continued through another full-span pass. Returning immediately removes that redundant traversal and overwrite of the destination.<\/p>\n<pre><code class=\"language-csharp\">\/\/ Run separately so each target uses its matching System.Numerics.Tensors package:\r\n\/\/ dotnet run -c Release -f net10.0 --filter \"*\"\r\n\/\/ dotnet run -c Release -f net11.0 --filter \"*\"\r\n\r\nusing System.Numerics.Tensors;\r\nusing BenchmarkDotNet.Attributes;\r\nusing BenchmarkDotNet.Running;\r\n\r\nBenchmarkSwitcher.FromAssembly(typeof(Benchmarks).Assembly).Run(args);\r\n\r\n[HideColumns(\"Job\", \"Error\", \"StdDev\", \"Median\", \"RatioSD\")]\r\npublic class Benchmarks\r\n{\r\n    private const int Length = 4096;\r\n    private readonly float[] _source = new float[Length];\r\n    private readonly float[] _destination = new float[Length];\r\n\r\n    [Benchmark]\r\n    public void RoundZero() =&gt; TensorPrimitives.Round(_source, 0, MidpointRounding.ToEven, _destination);\r\n}<\/code><\/pre>\n<table>\n<thead>\n<tr>\n<th>Method<\/th>\n<th>Runtime<\/th>\n<th>Mean<\/th>\n<th>Ratio<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td>RoundZero<\/td>\n<td>.NET 10.0<\/td>\n<td>882.7 ns<\/td>\n<td>1.00<\/td>\n<\/tr>\n<tr>\n<td>RoundZero<\/td>\n<td>.NET 11.0<\/td>\n<td>205.5 ns<\/td>\n<td>0.23<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<p><code>Half<\/code> comparisons are faster as well. Previously, <code>Half.CompareTo<\/code> separately\nasked whether one value was less than, greater than, or equal to the other,\nrepeating the special handling required for NaN and signed zero each time.\n<a href=\"https:\/\/github.com\/dotnet\/runtime\/pull\/131297\">dotnet\/runtime#131297<\/a> performs\nthat work once and then arranges the underlying bits into a form that can be\ncompared directly, while still treating <code>+0<\/code> and <code>-0<\/code> as equal. On x64 with\nAVX2, it also makes <code>CompareTo<\/code>, <code>&lt;<\/code>, and <code>&lt;=<\/code> faster by converting the operands\nto <code>float<\/code>, which the hardware can do very efficiently. Equality remains\nbit-based, as that&#8217;s already the cheaper approach.<\/p>\n<pre><code class=\"language-csharp\">\/\/ Run on x64 with AVX2:\r\n\/\/ dotnet run -c Release -f net10.0 --filter \"*\" --runtimes net10.0 net11.0\r\n\r\nusing BenchmarkDotNet.Attributes;\r\nusing BenchmarkDotNet.Running;\r\n\r\nBenchmarkSwitcher.FromAssembly(typeof(Benchmarks).Assembly).Run(args);\r\n\r\n[HideColumns(\"Job\", \"Error\", \"StdDev\", \"Median\", \"RatioSD\")]\r\npublic class Benchmarks\r\n{\r\n    private readonly Half[] _left = Enumerable.Range(0, 4096).Select(i =&gt; (Half)(i - 2048)).ToArray();\r\n    private readonly Half[] _right = Enumerable.Range(0, 4096).Select(i =&gt; (Half)(2048 - i)).ToArray();\r\n\r\n    [Benchmark]\r\n    public int CompareTo()\r\n    {\r\n        int sum = 0;\r\n        for (int i = 0; i &lt; _left.Length; i++)\r\n            sum += _left[i].CompareTo(_right[i]);\r\n\r\n        return sum;\r\n    }\r\n\r\n    [Benchmark]\r\n    public int LessThan()\r\n    {\r\n        int count = 0;\r\n        for (int i = 0; i &lt; _left.Length; i++)\r\n            count += _left[i] &lt; _right[i] ? 1 : 0;\r\n\r\n        return count;\r\n    }\r\n}<\/code><\/pre>\n<table>\n<thead>\n<tr>\n<th>Method<\/th>\n<th>Runtime<\/th>\n<th style=\"text-align: right;\">Mean<\/th>\n<th style=\"text-align: right;\">Ratio<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td>CompareTo<\/td>\n<td>.NET 10.0<\/td>\n<td style=\"text-align: right;\">9.340 \u03bcs<\/td>\n<td style=\"text-align: right;\">1.00<\/td>\n<\/tr>\n<tr>\n<td>CompareTo<\/td>\n<td>.NET 11.0<\/td>\n<td style=\"text-align: right;\">5.745 \u03bcs<\/td>\n<td style=\"text-align: right;\">0.62<\/td>\n<\/tr>\n<tr>\n<td><\/td>\n<td><\/td>\n<td style=\"text-align: right;\"><\/td>\n<td style=\"text-align: right;\"><\/td>\n<\/tr>\n<tr>\n<td>LessThan<\/td>\n<td>.NET 10.0<\/td>\n<td style=\"text-align: right;\">7.514 \u03bcs<\/td>\n<td style=\"text-align: right;\">1.00<\/td>\n<\/tr>\n<tr>\n<td>LessThan<\/td>\n<td>.NET 11.0<\/td>\n<td style=\"text-align: right;\">5.672 \u03bcs<\/td>\n<td style=\"text-align: right;\">0.75<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<p>Multiplying two 64-bit integers produces a 128-bit result, and x64 has instructions that provide both 64-bit halves directly. <a href=\"https:\/\/github.com\/dotnet\/runtime\/pull\/117261\">dotnet\/runtime#117261<\/a> from <a href=\"https:\/\/github.com\/Daniel-Svensson\">@Daniel-Svensson<\/a> exposes those signed and unsigned forms through an <code>X86Base.X64.BigMul<\/code> intrinsic. <code>Math.BigMul<\/code> can then map directly to <code>imul<\/code> or <code>mul<\/code> and return both halves in registers, avoiding the extra instructions and register shuffling required by the previous paths.<\/p>\n<pre><code class=\"language-csharp\">\/\/ Run on x64:\r\n\/\/ dotnet run -c Release -f net10.0 --filter \"*\" --runtimes net10.0 net11.0\r\n\r\nusing BenchmarkDotNet.Attributes;\r\nusing BenchmarkDotNet.Running;\r\n\r\nBenchmarkSwitcher.FromAssembly(typeof(Benchmarks).Assembly).Run(args);\r\n\r\n[DisassemblyDiagnoser, HideColumns(\"Job\", \"Error\", \"StdDev\", \"Median\", \"RatioSD\")]\r\npublic class Benchmarks\r\n{\r\n    private readonly long _signedLeft = 0x1234_5678_9ABC_DEF;\r\n    private readonly long _signedRight = 0x0FED_CBA9_8765_432;\r\n    private readonly ulong _unsignedLeft = 0xFEDC_BA98_7654_3210;\r\n    private readonly ulong _unsignedRight = 0x1234_5678_9ABC_DEF0;\r\n\r\n    [Benchmark]\r\n    public long Signed()\r\n    {\r\n        long high = Math.BigMul(_signedLeft, _signedRight, out long low);\r\n        return high ^ low;\r\n    }\r\n\r\n    [Benchmark]\r\n    public ulong Unsigned()\r\n    {\r\n        ulong high = Math.BigMul(_unsignedLeft, _unsignedRight, out ulong low);\r\n        return high ^ low;\r\n    }\r\n}<\/code><\/pre>\n<table>\n<thead>\n<tr>\n<th>Method<\/th>\n<th>Runtime<\/th>\n<th style=\"text-align: right;\">Mean<\/th>\n<th style=\"text-align: right;\">Ratio<\/th>\n<th style=\"text-align: right;\">Code Size<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td>Signed<\/td>\n<td>.NET 10.0<\/td>\n<td style=\"text-align: right;\">2.210 ns<\/td>\n<td style=\"text-align: right;\">1.00<\/td>\n<td style=\"text-align: right;\">65 B<\/td>\n<\/tr>\n<tr>\n<td>Signed<\/td>\n<td>.NET 11.0<\/td>\n<td style=\"text-align: right;\">1.344 ns<\/td>\n<td style=\"text-align: right;\">0.61<\/td>\n<td style=\"text-align: right;\">12 B<\/td>\n<\/tr>\n<tr>\n<td><\/td>\n<td><\/td>\n<td style=\"text-align: right;\"><\/td>\n<td style=\"text-align: right;\"><\/td>\n<td style=\"text-align: right;\"><\/td>\n<\/tr>\n<tr>\n<td>Unsigned<\/td>\n<td>.NET 10.0<\/td>\n<td style=\"text-align: right;\">1.446 ns<\/td>\n<td style=\"text-align: right;\">1.00<\/td>\n<td style=\"text-align: right;\">39 B<\/td>\n<\/tr>\n<tr>\n<td>Unsigned<\/td>\n<td>.NET 11.0<\/td>\n<td style=\"text-align: right;\">1.323 ns<\/td>\n<td style=\"text-align: right;\">0.92<\/td>\n<td style=\"text-align: right;\">12 B<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<p>Fixed-format numeric and identifier helpers benefit from a much simpler technique: establish the exact span length once, then let the JIT reuse that fact. <a href=\"https:\/\/github.com\/dotnet\/runtime\/pull\/119254\">dotnet\/runtime#119254<\/a> from <a href=\"https:\/\/github.com\/xtqqczze\">@xtqqczze<\/a> applies that pattern in <code>Decimal<\/code>, <code>Guid<\/code>, and <code>IPAddress<\/code>, removing repeated bounds checks.<\/p>\n<pre><code class=\"language-csharp\">\/\/ dotnet run -c Release -f net10.0 --filter \"*\" --runtimes net10.0 net11.0\r\n\r\nusing BenchmarkDotNet.Attributes;\r\nusing BenchmarkDotNet.Running;\r\n\r\nBenchmarkSwitcher.FromAssembly(typeof(Benchmarks).Assembly).Run(args);\r\n\r\n[HideColumns(\"Job\", \"Error\", \"StdDev\", \"Median\", \"RatioSD\")]\r\npublic class Benchmarks\r\n{\r\n    private const string Value = \"a8098c1a-f86e-11da-bd1a-00112444be1e\";\r\n\r\n    [Benchmark]\r\n    public bool TryParseExactD() =&gt; Guid.TryParseExact(Value, \"D\", out _);\r\n}<\/code><\/pre>\n<table>\n<thead>\n<tr>\n<th>Method<\/th>\n<th>Runtime<\/th>\n<th>Mean<\/th>\n<th>Ratio<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td>TryParseExactD<\/td>\n<td>.NET 10.0<\/td>\n<td>15.94 ns<\/td>\n<td>1.00<\/td>\n<\/tr>\n<tr>\n<td>TryParseExactD<\/td>\n<td>.NET 11.0<\/td>\n<td>12.60 ns<\/td>\n<td>0.79<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<p><code>Guid<\/code> has been improving every .NET release, and sees several improvements in .NET 11. Whenever possible, .NET tries to maintain similar performance and behaviors across operating systems, but low-level functionality often simply delegates to the operating system, exposing that OS&#8217; characteristics. When it comes to random number generation, historically cryptographically-secure random number generation, as is used in <code>Guid.NewGuid<\/code>, has been a bit slower on Linux than on Windows due to using <code>\/dev\/urandom<\/code> as the source of entropy. <a href=\"https:\/\/github.com\/dotnet\/runtime\/pull\/123540\">dotnet\/runtime#123540<\/a> from <a href=\"https:\/\/github.com\/reedz\">@reedz<\/a> moves <code>Guid.NewGuid()<\/code> off of that file-descriptor path to the <code>getrandom()<\/code> syscall, avoiding descriptor setup and reads through the file abstraction.<\/p>\n<p>And on the subject of randomness, <a href=\"https:\/\/github.com\/dotnet\/runtime\/pull\/119890\">dotnet\/runtime#119890<\/a> from <a href=\"https:\/\/github.com\/hamarb123\">@hamarb123<\/a> removes two pieces of work from <code>Random.Shuffle<\/code>: an unnecessary copy of the span length and a branch that skipped swapping an element with itself. A self-swap is harmless and uncommon, while testing for it adds an unpredictable branch to every iteration. The difference is most visible for short arrays and small value types, where the swap itself is cheap:<\/p>\n<pre><code class=\"language-csharp\">\/\/ dotnet run -c Release -f net10.0 --filter \"*\" --runtimes net10.0 net11.0\r\n\r\nusing BenchmarkDotNet.Attributes;\r\nusing BenchmarkDotNet.Running;\r\n\r\nBenchmarkSwitcher.FromAssembly(typeof(Benchmarks).Assembly).Run(args);\r\n\r\n[HideColumns(\"Job\", \"Error\", \"StdDev\", \"Median\", \"RatioSD\")]\r\npublic class Benchmarks\r\n{\r\n    [Params(16, 4096)]\r\n    public int Length;\r\n\r\n    private readonly Random _random = new(42);\r\n    private int[] _values = [];\r\n\r\n    [GlobalSetup]\r\n    public void Setup() =&gt; _values = Enumerable.Range(0, Length).ToArray();\r\n\r\n    [Benchmark]\r\n    public int ShuffleSmallValueType()\r\n    {\r\n        _random.Shuffle(_values);\r\n        return _values[0] + _values[^1];\r\n    }\r\n}<\/code><\/pre>\n<table>\n<thead>\n<tr>\n<th>Method<\/th>\n<th>Runtime<\/th>\n<th>Length<\/th>\n<th>Mean<\/th>\n<th>Ratio<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td>ShuffleSmallValueType<\/td>\n<td>.NET 10.0<\/td>\n<td>16<\/td>\n<td>138.9 ns<\/td>\n<td>1.00<\/td>\n<\/tr>\n<tr>\n<td>ShuffleSmallValueType<\/td>\n<td>.NET 11.0<\/td>\n<td>16<\/td>\n<td>89.34 ns<\/td>\n<td>0.64<\/td>\n<\/tr>\n<tr>\n<td>ShuffleSmallValueType<\/td>\n<td>.NET 10.0<\/td>\n<td>4096<\/td>\n<td>25,925.1 ns<\/td>\n<td>1.00<\/td>\n<\/tr>\n<tr>\n<td>ShuffleSmallValueType<\/td>\n<td>.NET 11.0<\/td>\n<td>4096<\/td>\n<td>14,151.08 ns<\/td>\n<td>0.55<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<p><code>Random<\/code> itself picked up a small but pointed code-generation fix. <code>Random.InternalSample<\/code> contains a condition that&#8217;s inherently hard for the processor to predict, so it&#8217;s better implemented with conditional instructions than with a branch. The JIT&#8217;s if-conversion support we previously discussed would have been able to do that transformation, except it doesn&#8217;t currently support if-conversion inside of loops, which is a pretty common place to find an inlined <code>Random.Next<\/code> call. <a href=\"https:\/\/github.com\/dotnet\/runtime\/pull\/131714\">dotnet\/runtime#131714<\/a> marks the helper as <code>[MethodImpl(MethodImplOptions.NoInlining)]<\/code> to preserve the branch-free form; once the JIT can perform if-conversion inside loops, that annotation can be reconsidered.<\/p>\n<pre><code class=\"language-csharp\">\/\/ dotnet run -c Release -f net10.0 --filter \"*\" --runtimes net10.0 net11.0\r\n\r\nusing BenchmarkDotNet.Attributes;\r\nusing BenchmarkDotNet.Running;\r\n\r\nBenchmarkSwitcher.FromAssembly(typeof(Benchmarks).Assembly).Run(args);\r\n\r\n[HideColumns(\"Job\", \"Error\", \"StdDev\", \"Median\", \"RatioSD\")]\r\npublic class Benchmarks\r\n{\r\n    private readonly Random _random = new(42);\r\n\r\n    [Benchmark]\r\n    public int Next()\r\n    {\r\n        int sum = 0;\r\n        for (int i = 0; i &lt; 1024; i++)\r\n            sum += _random.Next();\r\n\r\n        return sum;\r\n    }\r\n}<\/code><\/pre>\n<table>\n<thead>\n<tr>\n<th>Method<\/th>\n<th>Runtime<\/th>\n<th>Mean<\/th>\n<th>Ratio<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td>Next<\/td>\n<td>.NET 10.0<\/td>\n<td>5.954 \u03bcs<\/td>\n<td>1.00<\/td>\n<\/tr>\n<tr>\n<td>Next<\/td>\n<td>.NET 11.0<\/td>\n<td>3.275 \u03bcs<\/td>\n<td>0.55<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<h2>Globalization<\/h2>\n<p>Many globalization-related APIs sit atop data that can be expensive to locate\nand interpret. <code>DateTime.Now<\/code>, for example, depends on time-zone transition\ndata, while casing and parsing depend on native globalization services and\nculture-specific tables.<\/p>\n<p><a href=\"https:\/\/github.com\/dotnet\/runtime\/pull\/119662\">dotnet\/runtime#119662<\/a> substantially reworks <code>TimeZoneInfo<\/code> around that observation. Determining an offset isn&#8217;t always a fixed arithmetic operation: daylight-saving rules can vary by year, and historical rules can contain multiple transitions and exceptional cases. Once the transitions for a zone and year have been interpreted, however, other conversions in that year can reuse them. Similarly, the local offset used by <code>DateTime.Now<\/code> can&#8217;t change between transition instants. Conversions now reuse cached per-year transition data rather than repeatedly walking adjustment rules, while <code>DateTime.Now<\/code> caches the active UTC offset together with the instant at which it next needs to be recomputed.<\/p>\n<pre><code class=\"language-csharp\">\/\/ dotnet run -c Release -f net10.0 --filter \"*\" --runtimes net10.0 net11.0\r\n\r\nusing BenchmarkDotNet.Attributes;\r\nusing BenchmarkDotNet.Running;\r\n\r\nBenchmarkSwitcher.FromAssembly(typeof(Benchmarks).Assembly).Run(args);\r\n\r\n[HideColumns(\"Job\", \"Error\", \"StdDev\", \"Median\", \"RatioSD\")]\r\npublic class Benchmarks\r\n{\r\n    private readonly DateTime _utc = new(2026, 7, 15, 12, 0, 0, DateTimeKind.Utc);\r\n    private readonly DateTime _local = new(2026, 7, 15, 5, 0, 0, DateTimeKind.Unspecified);\r\n    private readonly TimeZoneInfo _zone = TimeZoneInfo.FindSystemTimeZoneById(\r\n        OperatingSystem.IsWindows() ? \"Pacific Standard Time\" : \"America\/Los_Angeles\");\r\n\r\n    [Benchmark]\r\n    public DateTime ConvertTimeFromUtc() =&gt; TimeZoneInfo.ConvertTimeFromUtc(_utc, _zone);\r\n\r\n    [Benchmark]\r\n    public DateTime ConvertTimeToUtc() =&gt; TimeZoneInfo.ConvertTimeToUtc(_local, _zone);\r\n\r\n    [Benchmark]\r\n    public DateTime GetLocalNow() =&gt; DateTime.Now;\r\n}<\/code><\/pre>\n<table>\n<thead>\n<tr>\n<th>Method<\/th>\n<th>Runtime<\/th>\n<th>Mean<\/th>\n<th>Ratio<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td>ConvertTimeFromUtc<\/td>\n<td>.NET 10.0<\/td>\n<td>45.13 ns<\/td>\n<td>1.00<\/td>\n<\/tr>\n<tr>\n<td>ConvertTimeFromUtc<\/td>\n<td>.NET 11.0<\/td>\n<td>19.44 ns<\/td>\n<td>0.43<\/td>\n<\/tr>\n<tr>\n<td>ConvertTimeToUtc<\/td>\n<td>.NET 10.0<\/td>\n<td>51.97 ns<\/td>\n<td>1.00<\/td>\n<\/tr>\n<tr>\n<td>ConvertTimeToUtc<\/td>\n<td>.NET 11.0<\/td>\n<td>20.23 ns<\/td>\n<td>0.39<\/td>\n<\/tr>\n<tr>\n<td>GetLocalNow<\/td>\n<td>.NET 10.0<\/td>\n<td>76.41 ns<\/td>\n<td>1.00<\/td>\n<\/tr>\n<tr>\n<td>GetLocalNow<\/td>\n<td>.NET 11.0<\/td>\n<td>34.39 ns<\/td>\n<td>0.45<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<p><a href=\"https:\/\/github.com\/dotnet\/runtime\/pull\/120685\">dotnet\/runtime#120685<\/a> separates two costs in invariant casing. With the normal globalization configuration, <code>ToUpperInvariant<\/code> and <code>ToLowerInvariant<\/code> now try a managed ASCII path first, so casing ASCII text can avoid or delay initialization of ICU, the native library .NET uses for culture-aware globalization. In invariant-globalization mode, where ICU isn&#8217;t loaded at all, that managed path also improves ASCII casing throughput. Non-ASCII input still needs the appropriate globalization path.<\/p>\n<pre><code class=\"language-csharp\">\/\/ DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=1 dotnet run -c Release -f net10.0 --filter \"*\" --runtimes net10.0 net11.0\r\n\r\nusing BenchmarkDotNet.Attributes;\r\nusing BenchmarkDotNet.Running;\r\n\r\nBenchmarkSwitcher.FromAssembly(typeof(Benchmarks).Assembly).Run(args);\r\n\r\n[HideColumns(\"Job\", \"Error\", \"StdDev\", \"Median\", \"RatioSD\")]\r\npublic class Benchmarks\r\n{\r\n    private readonly string _short = \"runtime\";\r\n    private readonly string _long = new('a', 139);\r\n\r\n    [Benchmark]\r\n    public string ShortAscii() =&gt; _short.ToUpperInvariant();\r\n\r\n    [Benchmark]\r\n    public string LongAscii() =&gt; _long.ToUpperInvariant();\r\n}<\/code><\/pre>\n<table>\n<thead>\n<tr>\n<th>Method<\/th>\n<th>Runtime<\/th>\n<th>Mean<\/th>\n<th>Ratio<\/th>\n<th>Allocated<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td>ShortAscii<\/td>\n<td>.NET 10.0<\/td>\n<td>18.12 ns<\/td>\n<td>1.00<\/td>\n<td>40 B<\/td>\n<\/tr>\n<tr>\n<td>ShortAscii<\/td>\n<td>.NET 11.0<\/td>\n<td>14.62 ns<\/td>\n<td>0.81<\/td>\n<td>40 B<\/td>\n<\/tr>\n<tr>\n<td>LongAscii<\/td>\n<td>.NET 10.0<\/td>\n<td>248.38 ns<\/td>\n<td>1.00<\/td>\n<td>304 B<\/td>\n<\/tr>\n<tr>\n<td>LongAscii<\/td>\n<td>.NET 11.0<\/td>\n<td>39.22 ns<\/td>\n<td>0.16<\/td>\n<td>304 B<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<p>Several smaller changes remove setup around date and culture data. <a href=\"https:\/\/github.com\/dotnet\/runtime\/pull\/123886\">dotnet\/runtime#123886<\/a> allocates the <code>DateTimeFormatInfo<\/code> date-word table only for cultures that actually contain such words. And <a href=\"https:\/\/github.com\/dotnet\/runtime\/pull\/122918\">dotnet\/runtime#122918<\/a> replaces synchronized, boxing <code>Hashtable<\/code> caches used by time-zone and encoding tables with typed <code>ConcurrentDictionary<\/code> instances.<\/p>\n<p>The round-trip <code>\"O\"<\/code> date format always contains exactly seven fractional-second digits, matching the 10,000,000 ticks in a second. <a href=\"https:\/\/github.com\/dotnet\/runtime\/pull\/129005\">dotnet\/runtime#129005<\/a> parses those digits directly as ticks, avoiding a conversion through <code>double<\/code> followed by division, multiplication, and rounding. Formatting benefits from specialization as well. <a href=\"https:\/\/github.com\/dotnet\/runtime\/pull\/129374\">dotnet\/runtime#129374<\/a> routes invariant <code>DateTime.ToString(\"G\")<\/code> through the existing fixed-format fast path, bypassing the general culture-aware formatter. <code>DateTimeOffset<\/code> retains the general path because its offset changes the output:<\/p>\n<pre><code class=\"language-csharp\">\/\/ dotnet run -c Release -f net10.0 --filter \"*\" --runtimes net10.0 net11.0\r\n\r\nusing BenchmarkDotNet.Running;\r\n\r\nusing System.Globalization;\r\nusing BenchmarkDotNet.Attributes;\r\n\r\nBenchmarkSwitcher.FromAssembly(typeof(Benchmarks).Assembly).Run(args);\r\n\r\n[HideColumns(\"Job\", \"Error\", \"StdDev\", \"Median\", \"RatioSD\")]\r\npublic class Benchmarks\r\n{\r\n    private readonly DateTime _dateTime = new(2024, 3, 15, 13, 45, 30, DateTimeKind.Utc);\r\n\r\n    [Benchmark]\r\n    public string DateTime_ToString_G() =&gt; _dateTime.ToString(\"G\", CultureInfo.InvariantCulture);\r\n}<\/code><\/pre>\n<table>\n<thead>\n<tr>\n<th>Method<\/th>\n<th>Runtime<\/th>\n<th>Mean<\/th>\n<th>Ratio<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td>DateTime_ToString_G<\/td>\n<td>.NET 10.0<\/td>\n<td>65.54 ns<\/td>\n<td>1.00<\/td>\n<\/tr>\n<tr>\n<td>DateTime_ToString_G<\/td>\n<td>.NET 11.0<\/td>\n<td>29.76 ns<\/td>\n<td>0.45<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<h2>Strings and Spans<\/h2>\n<p>UTF-8 is everywhere, from web protocols and JSON payloads to files on disk.\nSince .NET strings use UTF-16, applications frequently need to convert between\nthe two, making it especially important for those conversions to be fast.\nUTF-8 encoding must validate UTF-16 surrogate pairs as it counts and converts\nthem. On Arm64, the vectorized implementation in .NET 10 still examined\nindividual elements when counting the resulting UTF-8 bytes and checking that\nsurrogates were correctly paired. That gets expensive for text containing many\nsupplementary characters, as every surrogate-heavy vector falls back to this\nelement-by-element work.\n<a href=\"https:\/\/github.com\/dotnet\/runtime\/pull\/121981\">dotnet\/runtime#121981<\/a> from\n<a href=\"https:\/\/github.com\/ylpoonlg\">@ylpoonlg<\/a> instead performs the counting and\nsurrogate checks with vector-wide operations. As part of that work, it also\nunifies most of the x86 and Arm64 implementations, retaining small\nplatform-specific helpers where the instruction sets differ:<\/p>\n<pre><code class=\"language-csharp\">\/\/ dotnet run -c Release -f net10.0 --filter \"*\" --runtimes net10.0 net11.0\r\n\r\nusing BenchmarkDotNet.Attributes;\r\nusing BenchmarkDotNet.Running;\r\nusing System.Text;\r\n\r\nBenchmarkSwitcher.FromAssembly(typeof(Benchmarks).Assembly).Run(args);\r\n\r\n[HideColumns(\"Job\", \"Error\", \"StdDev\", \"Median\", \"RatioSD\")]\r\npublic class Benchmarks\r\n{\r\n    private const int Length = 4096;\r\n\r\n    private string _validWithSurrogatePairs = string.Empty;\r\n\r\n    [GlobalSetup]\r\n    public void Setup()\r\n    {\r\n        Random rng = new(42);\r\n        StringBuilder sb = new(Length);\r\n        while (sb.Length &lt; Length - 2)\r\n        {\r\n            sb.Append((char)('A' + rng.Next(0, 26)));\r\n            sb.Append(\"\\U0001F600\"); \/\/ emoji -&gt; surrogate pair\r\n        }\r\n\r\n        _validWithSurrogatePairs = sb.ToString();\r\n    }\r\n\r\n    [Benchmark]\r\n    public int ValidWithSurrogatePairs() =&gt; Encoding.UTF8.GetByteCount(_validWithSurrogatePairs);\r\n}<\/code><\/pre>\n<p>This input deliberately contains a surrogate pair for every ASCII character,\nmaking the removed per-element work especially visible.<\/p>\n<table>\n<thead>\n<tr>\n<th>Method<\/th>\n<th>Runtime<\/th>\n<th>Mean<\/th>\n<th>Ratio<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td>ValidWithSurrogatePairs<\/td>\n<td>.NET 10.0<\/td>\n<td>2.994 \u03bcs<\/td>\n<td>1.00<\/td>\n<\/tr>\n<tr>\n<td>ValidWithSurrogatePairs<\/td>\n<td>.NET 11.0<\/td>\n<td>856.8 ns<\/td>\n<td>0.29<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<p>The byte-to-char direction was also improved on Arm. UTF-8 decoding can copy ASCII bytes directly to UTF-16 characters, but as soon as we find the first non-ASCII byte, we need the full multi-byte decoder. The vector loop therefore needs both a fast test for whether any lane is non-ASCII and, only when one is found, its exact position. Calculating that position for every all-ASCII vector wastes work on the overwhelmingly common fast path. <a href=\"https:\/\/github.com\/dotnet\/runtime\/pull\/121382\">dotnet\/runtime#121382<\/a> from <a href=\"https:\/\/github.com\/ylpoonlg\">@ylpoonlg<\/a> first performs the cheap vector-wide test and then computes the lane index only after that test succeeds.<\/p>\n<pre><code class=\"language-csharp\">\/\/ dotnet run -c Release -f net10.0 --filter \"*\" --runtimes net10.0 net11.0\r\n\r\nusing BenchmarkDotNet.Attributes;\r\nusing BenchmarkDotNet.Running;\r\nusing System.Text;\r\n\r\nBenchmarkSwitcher.FromAssembly(typeof(Benchmarks).Assembly).Run(args);\r\n\r\n[HideColumns(\"Job\", \"Error\", \"StdDev\", \"Median\", \"RatioSD\")]\r\npublic class Benchmarks\r\n{\r\n    private readonly byte[] _ascii = Enumerable.Repeat((byte)'a', 16_384).ToArray();\r\n\r\n    [Benchmark]\r\n    public int Utf8GetCharCount() =&gt; Encoding.UTF8.GetCharCount(_ascii);\r\n}<\/code><\/pre>\n<p>With all-ASCII input, every vector can stay on the cheap path:<\/p>\n<table>\n<thead>\n<tr>\n<th>Method<\/th>\n<th>Runtime<\/th>\n<th>Mean<\/th>\n<th>Ratio<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td>Utf8GetCharCount<\/td>\n<td>.NET 10.0<\/td>\n<td>443.5 ns<\/td>\n<td>1.00<\/td>\n<\/tr>\n<tr>\n<td>Utf8GetCharCount<\/td>\n<td>.NET 11.0<\/td>\n<td>210.6 ns<\/td>\n<td>0.47<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<p>Base64 is commonly used when binary data needs to travel through\ntext-oriented formats and protocols. Its encoder naturally works in groups of\nthree input bytes and four output characters, but the line-breaking option\nalso needs to stop at the MIME-style 76-character boundary and insert <code>\\r\\n<\/code>.\nThe older implementation handled that formatting through a separate scalar\npath. <a href=\"https:\/\/github.com\/dotnet\/runtime\/pull\/123403\">dotnet\/runtime#123403<\/a> brings the optimized span-based Base64 encoder to <code>Convert.ToBase64String<\/code> with <code>Base64FormattingOptions.InsertLineBreaks<\/code>, processing each line with the same vectorized core and handling the separators around it:<\/p>\n<pre><code class=\"language-csharp\">\/\/ dotnet run -c Release -f net10.0 --filter \"*\" --runtimes net10.0 net11.0\r\n\r\nusing BenchmarkDotNet.Running;\r\n\r\nusing BenchmarkDotNet.Attributes;\r\n\r\nBenchmarkSwitcher.FromAssembly(typeof(Benchmarks).Assembly).Run(args);\r\n\r\n[HideColumns(\"Job\", \"Error\", \"StdDev\", \"Median\", \"RatioSD\")]\r\npublic class Benchmarks\r\n{\r\n    [Params(57, 570)]\r\n    public int ByteLength { get; set; }\r\n\r\n    private byte[] _bytes = [];\r\n\r\n    [GlobalSetup]\r\n    public void Setup()\r\n    {\r\n        _bytes = new byte[ByteLength];\r\n        new Random(42).NextBytes(_bytes);\r\n    }\r\n\r\n    [Benchmark]\r\n    public string ToBase64String_InsertLineBreaks() =&gt; Convert.ToBase64String(_bytes, Base64FormattingOptions.InsertLineBreaks);\r\n}<\/code><\/pre>\n<table>\n<thead>\n<tr>\n<th>Method<\/th>\n<th>Runtime<\/th>\n<th>ByteLength<\/th>\n<th>Mean<\/th>\n<th>Ratio<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td>ToBase64String_InsertLineBreaks<\/td>\n<td>.NET 10.0<\/td>\n<td>57<\/td>\n<td>60.95 ns<\/td>\n<td>1.00<\/td>\n<\/tr>\n<tr>\n<td>ToBase64String_InsertLineBreaks<\/td>\n<td>.NET 11.0<\/td>\n<td>57<\/td>\n<td>23.91 ns<\/td>\n<td>0.39<\/td>\n<\/tr>\n<tr>\n<td>ToBase64String_InsertLineBreaks<\/td>\n<td>.NET 10.0<\/td>\n<td>570<\/td>\n<td>560.66 ns<\/td>\n<td>1.00<\/td>\n<\/tr>\n<tr>\n<td>ToBase64String_InsertLineBreaks<\/td>\n<td>.NET 11.0<\/td>\n<td>570<\/td>\n<td>194.99 ns<\/td>\n<td>0.35<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<p>Base64 decoding got the same treatment from the other direction. <code>Base64.DecodeFromUtf8InPlace<\/code> decodes in place, overwriting the encoded input with the decoded bytes. In .NET 10, it still employed a scalar loop, long after the out-of-place <code>DecodeFromUtf8<\/code> had acquired AVX-512, AVX2, AdvSimd, and SSSE3 paths. In-place decoding turns out to be safe to vectorize precisely because of Base64&#8217;s ratio: 4 bytes read produce 3 bytes written, so the write cursor always trails the read cursor, and each vector store, including its zero-padded overshoot, ends at or before the next vector load and never clobbers source that hasn&#8217;t been read yet. <a href=\"https:\/\/github.com\/dotnet\/runtime\/pull\/131333\">dotnet\/runtime#131333<\/a> therefore reuses the existing decode helpers for the in-place path.<\/p>\n<pre><code class=\"language-csharp\">\/\/ dotnet run -c Release -f net10.0 --filter \"*\" --runtimes net10.0 net11.0\r\n\r\nusing System.Buffers;\r\nusing System.Buffers.Text;\r\nusing System.Text;\r\nusing BenchmarkDotNet.Attributes;\r\nusing BenchmarkDotNet.Running;\r\n\r\nBenchmarkSwitcher.FromAssembly(typeof(Benchmarks).Assembly).Run(args);\r\n\r\n[HideColumns(\"Job\", \"Error\", \"StdDev\", \"Median\", \"RatioSD\")]\r\npublic class Benchmarks\r\n{\r\n    private readonly byte[] _encoded = Encoding.ASCII.GetBytes(Convert.ToBase64String(new byte[16_384]));\r\n    private byte[] _buffer = [];\r\n\r\n    [IterationSetup]\r\n    public void Setup() =&gt; _buffer = (byte[])_encoded.Clone();\r\n\r\n    [Benchmark]\r\n    public OperationStatus Decode() =&gt; Base64.DecodeFromUtf8InPlace(_buffer, out _);\r\n}<\/code><\/pre>\n<table>\n<thead>\n<tr>\n<th>Method<\/th>\n<th>Runtime<\/th>\n<th>Mean<\/th>\n<th>Ratio<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td>Decode<\/td>\n<td>.NET 10.0<\/td>\n<td>10.70 \u03bcs<\/td>\n<td>1.00<\/td>\n<\/tr>\n<tr>\n<td>Decode<\/td>\n<td>.NET 11.0<\/td>\n<td>2.256 \u03bcs<\/td>\n<td>0.21<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<p><code>MemoryExtensions.CommonPrefixLength<\/code> compares two spans and returns how many\nelements they share at the beginning (&#8220;hello&#8221; and &#8220;help&#8221;, for example, have a\ncommon prefix length of 3). Internally, it utilizes a helper that slices whichever input was longer to\nthe length of the shorter one. <a href=\"https:\/\/github.com\/dotnet\/runtime\/pull\/121104\">dotnet\/runtime#121104<\/a>\nfrom <a href=\"https:\/\/github.com\/xtqqczze\">@xtqqczze<\/a> simplifies that helper: after\nshortening the second span if necessary, it always slices the first span to\nthe second&#8217;s length. That gives the JIT the same explicit relationship between\nthe two lengths regardless of which input started out longer.<\/p>\n<pre><code class=\"language-csharp\">\/\/ dotnet run -c Release -f net10.0 --filter \"*\" --runtimes net10.0 net11.0\r\n\r\nusing BenchmarkDotNet.Attributes;\r\nusing BenchmarkDotNet.Running;\r\nusing System;\r\nusing System.Linq;\r\n\r\nBenchmarkSwitcher.FromAssembly(typeof(Benchmarks).Assembly).Run(args);\r\n\r\n[HideColumns(\"Job\", \"Error\", \"StdDev\", \"Median\", \"RatioSD\")]\r\npublic class Benchmarks\r\n{\r\n    private readonly string[] _shorter = Enumerable.Repeat(\"value\", 64).ToArray();\r\n    private readonly string[] _longer = Enumerable.Repeat(\"value\", 128).ToArray();\r\n\r\n    [Benchmark]\r\n    public int ShorterFirst() =&gt; _shorter.AsSpan().CommonPrefixLength(_longer);\r\n\r\n    [Benchmark]\r\n    public int LongerFirst() =&gt; _longer.AsSpan().CommonPrefixLength(_shorter);\r\n}<\/code><\/pre>\n<p>The longer-first case was already efficient. The change improves the\nshorter-first case, bringing the two orderings to essentially the same\nthroughput:<\/p>\n<table>\n<thead>\n<tr>\n<th>Method<\/th>\n<th>Runtime<\/th>\n<th>Mean<\/th>\n<th>Ratio<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td>ShorterFirst<\/td>\n<td>.NET 10.0<\/td>\n<td>45.16 ns<\/td>\n<td>1.00<\/td>\n<\/tr>\n<tr>\n<td>ShorterFirst<\/td>\n<td>.NET 11.0<\/td>\n<td>25.43 ns<\/td>\n<td>0.56<\/td>\n<\/tr>\n<tr>\n<td>LongerFirst<\/td>\n<td>.NET 10.0<\/td>\n<td>26.40 ns<\/td>\n<td>1.00<\/td>\n<\/tr>\n<tr>\n<td>LongerFirst<\/td>\n<td>.NET 11.0<\/td>\n<td>26.31 ns<\/td>\n<td>1.00<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<p>Text processing often starts by obtaining an <code>Encoding<\/code>. Properties such as\n<code>Encoding.UTF8<\/code> provide fast access to popular encodings, while legacy code\npages can be made available by registering <code>CodePagesEncodingProvider<\/code>. In\n.NET 10, that provider&#8217;s tables, including the name lookup used by\n<code>Encoding.GetEncoding(string)<\/code> once the provider is registered, used\nreader-writer locks. <a href=\"https:\/\/github.com\/dotnet\/runtime\/pull\/125001\">dotnet\/runtime#125001<\/a>\nreplaces those caches with <code>ConcurrentDictionary<\/code> instances, allowing\nwarmed-up provider lookups to proceed without acquiring the reader lock.<\/p>\n<p>On <code>string<\/code> itself, <a href=\"https:\/\/github.com\/dotnet\/runtime\/pull\/130361\">dotnet\/runtime#130361<\/a> from <a href=\"https:\/\/github.com\/prozolic\">@prozolic<\/a> recognizes when <code>string.Concat(IEnumerable&lt;string?&gt;)<\/code> receives a <code>string[]<\/code> or <code>List&lt;string?&gt;<\/code> and passes its contiguous storage directly to the span-based implementation:<\/p>\n<pre><code class=\"language-csharp\">\/\/ dotnet run -c Release -f net10.0 --filter \"*\" --runtimes net10.0 net11.0\r\n\r\nusing System.Collections.Generic;\r\nusing System.Linq;\r\n\r\nusing BenchmarkDotNet.Attributes;\r\nusing BenchmarkDotNet.Running;\r\n\r\nBenchmarkSwitcher.FromAssembly(typeof(Benchmarks).Assembly).Run(args);\r\n\r\n[MemoryDiagnoser(false), HideColumns(\"Job\", \"Error\", \"StdDev\", \"Median\", \"RatioSD\")]\r\npublic class Benchmarks\r\n{\r\n    private readonly IEnumerable&lt;string?&gt; _array = Enumerable.Range(0, 1_000).Select(i =&gt; i.ToString()).ToArray();\r\n    private readonly IEnumerable&lt;string?&gt; _list = Enumerable.Range(0, 1_000).Select(i =&gt; i.ToString()).ToList();\r\n\r\n    [Benchmark]\r\n    public string Array() =&gt; string.Concat(_array);\r\n\r\n    [Benchmark]\r\n    public string List() =&gt; string.Concat(_list);\r\n}<\/code><\/pre>\n<table>\n<thead>\n<tr>\n<th>Method<\/th>\n<th>Runtime<\/th>\n<th>Mean<\/th>\n<th>Ratio<\/th>\n<th>Allocated<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td>Array<\/td>\n<td>.NET 10.0<\/td>\n<td>4.230 \u03bcs<\/td>\n<td>1.00<\/td>\n<td>5.7 KB<\/td>\n<\/tr>\n<tr>\n<td>Array<\/td>\n<td>.NET 11.0<\/td>\n<td>3.285 \u03bcs<\/td>\n<td>0.78<\/td>\n<td>5.67 KB<\/td>\n<\/tr>\n<tr>\n<td>List<\/td>\n<td>.NET 10.0<\/td>\n<td>6.703 \u03bcs<\/td>\n<td>1.00<\/td>\n<td>5.71 KB<\/td>\n<\/tr>\n<tr>\n<td>List<\/td>\n<td>.NET 11.0<\/td>\n<td>3.253 \u03bcs<\/td>\n<td>0.49<\/td>\n<td>5.67 KB<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<p>Some of my favorite improvements in .NET are the tiny ones that show up everywhere. A good example of that is in <a href=\"https:\/\/github.com\/dotnet\/roslyn\/pull\/82729\">dotnet\/roslyn#82729<\/a>. Previously, when you wrote <code>span[start..]<\/code>, the compiler would lower that to the equivalent of <code>span.Slice(start, span.Length - start)<\/code>. The JIT has made strides towards compiling this exactly how it would <code>span.Slice(start)<\/code>, but everyone is better off if the C# compiler just emits that in the first place. And it now does. The difference is clear in the IL for a method that returns <code>span[start..]<\/code>:<\/p>\n<pre><code class=\"language-diff\">; Platform-independent IL\r\n-\/\/ Before: 21 bytes\r\n+\/\/ After: 9 bytes\r\n-.locals init ([0] System.Span&lt;char&gt;&amp;, [1] int32)\r\n ldarga.s span\r\n-stloc.0\r\n ldarg.1\r\n-stloc.1\r\n-ldloc.0\r\n-ldloc.1\r\n-ldloc.0\r\n-call instance int32 System.Span&lt;char&gt;::get_Length()\r\n-ldloc.1\r\n-sub\r\n-call instance System.Span&lt;char&gt; System.Span&lt;char&gt;::Slice(int32, int32)\r\n+call instance System.Span&lt;char&gt; System.Span&lt;char&gt;::Slice(int32)\r\n ret<\/code><\/pre>\n<h3>Searching and Comparing<\/h3>\n<p>Searching in one way, shape, or form is one of the most common things programs do. And when it comes to searching text, regular expressions are an extremely common and helpful way to specify and perform said search. .NET&#8217;s regex support has improved by leaps and bounds over the years, with significant investments in .NET 5 and .NET 7 and then every release since, including .NET 11.<\/p>\n<p>When a <code>Regex<\/code> instance is created, it needs to parse the incoming regular expression pattern and turn it into a form it can utilize for performing the actual searches. The regex language is very expressive and enables multiple ways of specifying the same pattern, some more efficient to process than others, so as part of parsing, <code>Regex<\/code> applies a variety of simplifications and optimizations over the parsed tree in order to put it into an ideal form, as well as to learn facts about the pattern to further optimize later processing (such as discovering a minimum and maximum length of any possible match). Each of these transformations can in turn expose more opportunity for other transformations, but based on the order the transformations are applied, sometimes those opportunities can be missed. In .NET 11, <a href=\"https:\/\/github.com\/dotnet\/runtime\/pull\/125289\">dotnet\/runtime#125289<\/a> gives compiled and source-generated regexes one final cleanup pass after the whole-pattern optimizations have reshaped the pattern. Consider the pattern <code>[ab]+c[ab]+|[ab]+<\/code>. On input containing a long run of <code>a<\/code>s with no <code>c<\/code>, the .NET 10 source-generated matcher first scans the whole run for the first alternative, fails when it doesn&#8217;t find the <code>c<\/code>, and then scans the same run again for the second alternative. The final cleanup pass factors out the common <code>[ab]+<\/code>, leaving <code>c[ab]+<\/code> as an optional suffix:<\/p>\n<pre><code class=\"language-csharp\">\/\/ dotnet run -c Release -f net10.0 --filter \"*\"\r\n\/\/ dotnet run -c Release -f net11.0 --filter \"*\"\r\n\r\nusing System.Text.RegularExpressions;\r\n\r\nusing BenchmarkDotNet.Attributes;\r\nusing BenchmarkDotNet.Running;\r\n\r\nBenchmarkSwitcher.FromAssembly(typeof(Benchmarks).Assembly).Run(args);\r\n\r\n[HideColumns(\"Job\", \"Error\", \"StdDev\", \"Median\", \"RatioSD\")]\r\npublic partial class Benchmarks\r\n{\r\n    private readonly string _input = new('a', 4096);\r\n\r\n    [Benchmark]\r\n    public bool SharedPrefix() =&gt; SharedPrefixRegex().IsMatch(_input);\r\n\r\n    [GeneratedRegex(\"[ab]+c[ab]+|[ab]+\")]\r\n    private static partial Regex SharedPrefixRegex();\r\n}<\/code><\/pre>\n<table>\n<thead>\n<tr>\n<th>Method<\/th>\n<th>Runtime<\/th>\n<th>Mean<\/th>\n<th>Ratio<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td>SharedPrefix<\/td>\n<td>.NET 10.0<\/td>\n<td>550.9 ns<\/td>\n<td>1.00<\/td>\n<\/tr>\n<tr>\n<td>SharedPrefix<\/td>\n<td>.NET 11.0<\/td>\n<td>282.8 ns<\/td>\n<td>0.51<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<p>Beyond doing additional passes, several other changes improve what those\nanalysis passes can see. For example, for a pattern like <code>(http|https)<\/code> with\nordinal ignore-case matching, for uninteresting reasons previously the engine\nwould extract a prefix of <code>\"htt\"<\/code>, even though it could have extracted\n<code>\"http\"<\/code>. <a href=\"https:\/\/github.com\/dotnet\/runtime\/pull\/124881\">dotnet\/runtime#124881<\/a>\nimproves that, enabling the engine to skip far more false candidates. The\ninput here contains 25,000 <code>\"htt\"<\/code> prefixes that aren&#8217;t followed by a <code>p<\/code>\nbefore the final match:<\/p>\n<pre><code class=\"language-csharp\">\/\/ dotnet run -c Release -f net10.0 --filter \"*\"\r\n\/\/ dotnet run -c Release -f net11.0 --filter \"*\"\r\n\r\nusing System.Text.RegularExpressions;\r\n\r\nusing BenchmarkDotNet.Attributes;\r\nusing BenchmarkDotNet.Running;\r\n\r\nBenchmarkSwitcher.FromAssembly(typeof(Benchmarks).Assembly).Run(args);\r\n\r\n[HideColumns(\"Job\", \"Error\", \"StdDev\", \"Median\", \"RatioSD\")]\r\npublic partial class Benchmarks\r\n{\r\n    private readonly string _input = string.Concat(Enumerable.Repeat(\"httx\", 25_000)) + \"https\";\r\n\r\n    [Benchmark]\r\n    public bool IgnoreCaseAlternation() =&gt; Http.IsMatch(_input);\r\n\r\n    [GeneratedRegex(\"(http|https)\", RegexOptions.IgnoreCase)]\r\n    private static partial Regex Http { get; }\r\n}<\/code><\/pre>\n<table>\n<thead>\n<tr>\n<th>Method<\/th>\n<th>Runtime<\/th>\n<th>Mean<\/th>\n<th>Ratio<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td>IgnoreCaseAlternation<\/td>\n<td>.NET 10.0<\/td>\n<td>415.0 \u03bcs<\/td>\n<td>1.00<\/td>\n<\/tr>\n<tr>\n<td>IgnoreCaseAlternation<\/td>\n<td>.NET 11.0<\/td>\n<td>7.012 \u03bcs<\/td>\n<td>0.017<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<p>When those transformation passes are looking for various patterns, sometimes small\nthings obscure what they&#8217;re trying to see, and they miss optimizations.\n<a href=\"https:\/\/github.com\/dotnet\/runtime\/pull\/124842\">dotnet\/runtime#124842<\/a>\nimproves a case where captures were getting in the way of identifying a\nsearchable prefix. For a pattern like <code>\\b(in)\\b<\/code> with\n<code>RegexOptions.IgnoreCase<\/code>, it will now discover it can search for\nordinal-ignore-case <code>\"in\"<\/code>.<\/p>\n<pre><code class=\"language-csharp\">\/\/ dotnet run -c Release -f net10.0 --filter \"*\"\r\n\/\/ dotnet run -c Release -f net11.0 --filter \"*\"\r\n\r\nusing System.Text.RegularExpressions;\r\n\r\nusing BenchmarkDotNet.Attributes;\r\nusing BenchmarkDotNet.Running;\r\n\r\nBenchmarkSwitcher.FromAssembly(typeof(Benchmarks).Assembly).Run(args);\r\n\r\n[HideColumns(\"Job\", \"Error\", \"StdDev\", \"Median\", \"RatioSD\")]\r\npublic partial class Benchmarks\r\n{\r\n    private readonly string _input = string.Concat(Enumerable.Repeat(\"xn \", 33_333)) + \"in\";\r\n\r\n    [Benchmark]\r\n    public bool IgnoreCaseCapturedPrefix() =&gt; CapturedPrefix.IsMatch(_input);\r\n\r\n    [GeneratedRegex(@\"\\b(in)\\b\", RegexOptions.IgnoreCase)]\r\n    private static partial Regex CapturedPrefix { get; }\r\n}<\/code><\/pre>\n<table>\n<thead>\n<tr>\n<th>Method<\/th>\n<th>Runtime<\/th>\n<th>Mean<\/th>\n<th>Ratio<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td>IgnoreCaseCapturedPrefix<\/td>\n<td>.NET 10.0<\/td>\n<td>277.7 \u03bcs<\/td>\n<td>1.00<\/td>\n<\/tr>\n<tr>\n<td>IgnoreCaseCapturedPrefix<\/td>\n<td>.NET 11.0<\/td>\n<td>7.286 \u03bcs<\/td>\n<td>0.026<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<p>As these cases highlight, one of the most impactful things we can do for regular expression processing is improve the engine&#8217;s ability to find things to search for as the next possible place a match could apply, and to optimize that search. <a href=\"https:\/\/github.com\/dotnet\/runtime\/pull\/124736\">dotnet\/runtime#124736<\/a> does that. For compiled, source-generated, and <code>NonBacktracking<\/code> regexes, it improves how the engine is able to search for one of several literal prefixes. For <code>agggtaaa|tttaccct<\/code>, for example, the .NET 10 source generator first searched for <code>[ag]<\/code> at offset 3 and then checked nearby characters for <code>[gt]<\/code>. That&#8217;s a weak filter for an input full of <code>a<\/code> characters, where almost every position becomes a candidate. The .NET 11 generator instead searches for the complete <code>agggtaaa<\/code> and <code>tttaccct<\/code> strings with <code>SearchValues&lt;string&gt;<\/code>. A frequency heuristic selects this approach only for case-sensitive alternatives where whole-string searching is expected to reject more false candidates than the available character-set filter.<\/p>\n<pre><code class=\"language-csharp\">\/\/ dotnet run -c Release -f net10.0 --filter \"*\"\r\n\/\/ dotnet run -c Release -f net11.0 --filter \"*\"\r\n\r\nusing BenchmarkDotNet.Attributes;\r\nusing BenchmarkDotNet.Running;\r\n\r\nusing System.Text.RegularExpressions;\r\n\r\nBenchmarkSwitcher.FromAssembly(typeof(RegexPrefixBenchmarks).Assembly).Run(args);\r\n\r\n[HideColumns(\"Job\", \"Error\", \"StdDev\", \"Median\", \"RatioSD\")]\r\npublic partial class RegexPrefixBenchmarks\r\n{\r\n    private const string Pattern = \"agggtaaa|tttaccct\";\r\n\r\n    private readonly string _match = new string('a', 100_000) + \"tttaccct\";\r\n    private readonly string _miss = new('a', 100_000);\r\n\r\n    [Benchmark]\r\n    public bool Match() =&gt; Generated.IsMatch(_match);\r\n\r\n    [Benchmark]\r\n    public bool Miss() =&gt; Generated.IsMatch(_miss);\r\n\r\n    [GeneratedRegex(Pattern)]\r\n    private static partial Regex Generated { get; }\r\n}<\/code><\/pre>\n<table>\n<thead>\n<tr>\n<th>Method<\/th>\n<th>Runtime<\/th>\n<th>Mean<\/th>\n<th>Ratio<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td>Match<\/td>\n<td>.NET 10.0<\/td>\n<td>861.9 \u03bcs<\/td>\n<td>1.00<\/td>\n<\/tr>\n<tr>\n<td>Match<\/td>\n<td>.NET 11.0<\/td>\n<td>9.251 \u03bcs<\/td>\n<td>0.011<\/td>\n<\/tr>\n<tr>\n<td>Miss<\/td>\n<td>.NET 10.0<\/td>\n<td>861.4 \u03bcs<\/td>\n<td>1.00<\/td>\n<\/tr>\n<tr>\n<td>Miss<\/td>\n<td>.NET 11.0<\/td>\n<td>9.647 \u03bcs<\/td>\n<td>0.011<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<p>Of course, searching for the next place to match isn&#8217;t the only opportunity for improvement. Once you&#8217;ve found that place, you need to try to match, and we want to optimize that further, too.<\/p>\n<p>Consider the pattern <code>\\b\\w+n\\b<\/code>. The <code>\\w+<\/code> can match <code>n<\/code>, which means we can&#8217;t automatically treat this loop as being atomic. Normally, after matching the loop greedily and failing to match <code>n<\/code>, we&#8217;d need to backtrack looking for the next viable place to match the <code>n<\/code>. But if what comes after the <code>n<\/code> (in this case, a boundary) can&#8217;t possibly match the loop, we can avoid doing that search. <a href=\"https:\/\/github.com\/dotnet\/runtime\/pull\/125636\">dotnet\/runtime#125636<\/a> teaches the compiled and source-generated engines to prove that and test the final position directly rather than searching backward through the loop&#8217;s existing match. The same idea applies to other loops followed by a literal when the engine can prove that trying earlier positions can&#8217;t change the result.<\/p>\n<pre><code class=\"language-csharp\">\/\/ dotnet run -c Release -f net10.0 --filter \"*\"\r\n\/\/ dotnet run -c Release -f net11.0 --filter \"*\"\r\n\r\nusing BenchmarkDotNet.Running;\r\n\r\nusing System.Text.RegularExpressions;\r\nusing BenchmarkDotNet.Attributes;\r\n\r\nBenchmarkSwitcher.FromAssembly(typeof(Benchmarks).Assembly).Run(args);\r\n\r\n[HideColumns(\"Job\", \"Error\", \"StdDev\", \"Median\", \"RatioSD\")]\r\npublic partial class Benchmarks\r\n{\r\n    private const int WordLength = 5000;\r\n    private readonly string _matchingWord = new string('a', WordLength - 1) + \"n\";\r\n    private readonly string _nonMatchingWord = new string('a', WordLength - 1) + \"b\";\r\n\r\n    [GeneratedRegex(@\"\\b\\w+n\\b\")]\r\n    private static partial Regex Generated { get; }\r\n\r\n    [Benchmark]\r\n    public bool Matching() =&gt; Generated.IsMatch(_matchingWord);\r\n\r\n    [Benchmark]\r\n    public bool NonMatching() =&gt; Generated.IsMatch(_nonMatchingWord);\r\n}<\/code><\/pre>\n<table>\n<thead>\n<tr>\n<th>Method<\/th>\n<th>Runtime<\/th>\n<th>Mean<\/th>\n<th>Ratio<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td>Matching<\/td>\n<td>.NET 10.0<\/td>\n<td>3.441 \u03bcs<\/td>\n<td>1.00<\/td>\n<\/tr>\n<tr>\n<td>Matching<\/td>\n<td>.NET 11.0<\/td>\n<td>3.118 \u03bcs<\/td>\n<td>0.91<\/td>\n<\/tr>\n<tr>\n<td>NonMatching<\/td>\n<td>.NET 10.0<\/td>\n<td>83.656 \u03bcs<\/td>\n<td>1.00<\/td>\n<\/tr>\n<tr>\n<td>NonMatching<\/td>\n<td>.NET 11.0<\/td>\n<td>69.984 \u03bcs<\/td>\n<td>0.84<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<p>A match can sometimes be ruled out before examining any of the input&#8217;s\ncharacters. When matching starts at position zero, a fixed-length pattern with\na leading <code>\\A<\/code> or non-multiline <code>^<\/code> and a trailing <code>\\z<\/code> can match only when the\nwhole input has exactly that length.\n<a href=\"https:\/\/github.com\/dotnet\/runtime\/pull\/120916\">dotnet\/runtime#120916<\/a> emits\nthat length check up front for the compiled and source-generated engines when\nthe computed maximum length equals the minimum required length. Here, the\npattern requires exactly 512 characters while the input contains 513:<\/p>\n<pre><code class=\"language-csharp\">\/\/ dotnet run -c Release -f net10.0 --filter \"*\"\r\n\/\/ dotnet run -c Release -f net11.0 --filter \"*\"\r\n\r\nusing BenchmarkDotNet.Attributes;\r\nusing BenchmarkDotNet.Running;\r\n\r\nusing System.Text.RegularExpressions;\r\n\r\nBenchmarkSwitcher.FromAssembly(typeof(Benchmarks).Assembly).Run(args);\r\n\r\n[HideColumns(\"Job\", \"Error\", \"StdDev\", \"Median\", \"RatioSD\")]\r\npublic partial class Benchmarks\r\n{\r\n    private readonly string _tooLong = new('a', 513);\r\n\r\n    [GeneratedRegex(@\"\\A[a-z]{512}\\z\")]\r\n    private static partial Regex Generated { get; }\r\n\r\n    [Benchmark]\r\n    public bool AnchoredReject() =&gt; Generated.IsMatch(_tooLong);\r\n}<\/code><\/pre>\n<table>\n<thead>\n<tr>\n<th>Method<\/th>\n<th>Runtime<\/th>\n<th>Mean<\/th>\n<th>Ratio<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td>AnchoredReject<\/td>\n<td>.NET 10.0<\/td>\n<td>41.06 ns<\/td>\n<td>1.00<\/td>\n<\/tr>\n<tr>\n<td>AnchoredReject<\/td>\n<td>.NET 11.0<\/td>\n<td>16.05 ns<\/td>\n<td>0.39<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<p>In general, we&#8217;ve tried to keep the compilers behind <code>RegexOptions.Compiled<\/code> (which emits IL) and the source generator (which emits C#) as close to 1:1 as possible. There are a few cases, however, where they have diverged from each other, generally where one was able to easily utilize some feature of the target language the other didn&#8217;t have. A good example is with alternations. If several left-to-right atomic branches each begin with a different literal character, the engine can read that character and jump straight to the matching branch rather than testing each branch in order. With C#, we emitted a <code>switch<\/code>, which the C# compiler could then lower to IL using various strategies. For IL, in .NET 10 and earlier, without the C# compiler to provide those optimizations, we just skipped the optimization. Now in .NET 11, <a href=\"https:\/\/github.com\/dotnet\/runtime\/pull\/122959\">dotnet\/runtime#122959<\/a> emits a similar implementation to what the C# compiler would have, bringing this optimization to <code>RegexOptions.Compiled<\/code>.<\/p>\n<pre><code class=\"language-csharp\">\/\/ dotnet run -c Release -f net10.0 --filter \"*\" --runtimes net10.0 net11.0\r\n\r\nusing System.Text.RegularExpressions;\r\n\r\nusing BenchmarkDotNet.Attributes;\r\nusing BenchmarkDotNet.Running;\r\n\r\nBenchmarkSwitcher.FromAssembly(typeof(Benchmarks).Assembly).Run(args);\r\n\r\n[HideColumns(\"Job\", \"Error\", \"StdDev\", \"Median\", \"RatioSD\")]\r\npublic class Benchmarks\r\n{\r\n    private readonly string _input = string.Concat(Enumerable.Repeat(\"p15\", 10_000));\r\n    private readonly Regex _regex = new(@\"(?&gt;a0|b1|c2|d3|e4|f5|g6|h7|i8|j9|k10|l11|m12|n13|o14|p15)\", RegexOptions.Compiled);\r\n\r\n    [Benchmark]\r\n    public int DispatchToFinalBranch() =&gt; _regex.Count(_input);\r\n}<\/code><\/pre>\n<table>\n<thead>\n<tr>\n<th>Method<\/th>\n<th>Runtime<\/th>\n<th>Mean<\/th>\n<th>Ratio<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td>DispatchToFinalBranch<\/td>\n<td>.NET 10.0<\/td>\n<td>233.9 \u03bcs<\/td>\n<td>1.00<\/td>\n<\/tr>\n<tr>\n<td>DispatchToFinalBranch<\/td>\n<td>.NET 11.0<\/td>\n<td>159.5 \u03bcs<\/td>\n<td>0.68<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<p>Another of the few differences between compiled and source-generated regexes had to do with backreferences. A case-sensitive backreference, such as the <code>\\1<\/code> in <code>([a-z]+)-\\1<\/code>, asks whether the next input equals text that was previously captured in the match. Source-generated regexes were using the optimized <code>SequenceEqual<\/code> to do that comparison, whereas <code>RegexOptions.Compiled<\/code> wasn&#8217;t. With <a href=\"https:\/\/github.com\/dotnet\/runtime\/pull\/123914\">dotnet\/runtime#123914<\/a> in .NET 11, now it does.<\/p>\n<pre><code class=\"language-csharp\">\/\/ dotnet run -c Release -f net10.0 --filter \"*\" --runtimes net10.0 net11.0\r\n\r\nusing BenchmarkDotNet.Attributes;\r\nusing BenchmarkDotNet.Running;\r\n\r\nusing System.Text.RegularExpressions;\r\n\r\nBenchmarkSwitcher.FromAssembly(typeof(Benchmarks).Assembly).Run(args);\r\n\r\n[HideColumns(\"Job\", \"Error\", \"StdDev\", \"Median\", \"RatioSD\")]\r\npublic class Benchmarks\r\n{\r\n    private readonly string _input = new string('a', 256) + \"-\" + new string('a', 256);\r\n    private readonly Regex _regex = new(@\"^([a-z]{256})-\\1$\", RegexOptions.Compiled);\r\n\r\n    [Benchmark]\r\n    public bool Backreference() =&gt; _regex.IsMatch(_input);\r\n}<\/code><\/pre>\n<table>\n<thead>\n<tr>\n<th>Method<\/th>\n<th>Runtime<\/th>\n<th>Mean<\/th>\n<th>Ratio<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td>Backreference<\/td>\n<td>.NET 10.0<\/td>\n<td>168.4 ns<\/td>\n<td>1.00<\/td>\n<\/tr>\n<tr>\n<td>Backreference<\/td>\n<td>.NET 11.0<\/td>\n<td>49.59 ns<\/td>\n<td>0.29<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<p>Searching isn&#8217;t limited to <code>Regex<\/code>, of course. Many other methods in .NET help finding things and comparing things, some of which get notable bumps in .NET 11.<\/p>\n<p>The <code>Ascii<\/code> class provides optimized helpers for validating and manipulating ASCII text. Members like <code>Equals<\/code> are already vectorized in .NET 10, but in .NET 11, <a href=\"https:\/\/github.com\/dotnet\/runtime\/pull\/123115\">dotnet\/runtime#123115<\/a> improves that implementation by ensuring that inputs of length 8 through 15 can be vectorized, as well.<\/p>\n<pre><code class=\"language-csharp\">\/\/ dotnet run -c Release -f net10.0 --filter \"*\" --runtimes net10.0 net11.0\r\n\r\nusing BenchmarkDotNet.Running;\r\n\r\nusing BenchmarkDotNet.Attributes;\r\nusing System.Text;\r\n\r\nBenchmarkSwitcher.FromAssembly(typeof(Benchmarks).Assembly).Run(args);\r\n\r\n[HideColumns(\"Job\", \"Error\", \"StdDev\", \"Median\", \"RatioSD\")]\r\npublic class Benchmarks\r\n{\r\n    [Params(8, 15)]\r\n    public int Length { get; set; }\r\n\r\n    private byte[] _bytes = [];\r\n    private char[] _charsMatching = [];\r\n\r\n    [GlobalSetup]\r\n    public void Setup()\r\n    {\r\n        _bytes = new byte[Length];\r\n        _charsMatching = new char[Length];\r\n        for (int i = 0; i &lt; Length; i++)\r\n        {\r\n            byte b = (byte)('a' + (i % 26));\r\n            _bytes[i] = b;\r\n            _charsMatching[i] = (char)b;\r\n        }\r\n    }\r\n\r\n    [Benchmark]\r\n    public bool Equals_Matching() =&gt; Ascii.Equals(_bytes, _charsMatching);\r\n}<\/code><\/pre>\n<table>\n<thead>\n<tr>\n<th>Method<\/th>\n<th>Runtime<\/th>\n<th>Length<\/th>\n<th>Mean<\/th>\n<th>Ratio<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td>Equals_Matching<\/td>\n<td>.NET 10.0<\/td>\n<td>8<\/td>\n<td>3.834 ns<\/td>\n<td>1.00<\/td>\n<\/tr>\n<tr>\n<td>Equals_Matching<\/td>\n<td>.NET 11.0<\/td>\n<td>8<\/td>\n<td>1.966 ns<\/td>\n<td>0.51<\/td>\n<\/tr>\n<tr>\n<td>Equals_Matching<\/td>\n<td>.NET 10.0<\/td>\n<td>15<\/td>\n<td>6.177 ns<\/td>\n<td>1.00<\/td>\n<\/tr>\n<tr>\n<td>Equals_Matching<\/td>\n<td>.NET 11.0<\/td>\n<td>15<\/td>\n<td>2.398 ns<\/td>\n<td>0.39<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<p><a href=\"https:\/\/github.com\/dotnet\/runtime\/pull\/130644\">dotnet\/runtime#130644<\/a>\nalso improves equality performance, in this case with <code>SequenceEqual<\/code> over a\nspan of <code>Guid<\/code> or <code>Int128<\/code>. Previously, <code>SequenceEqual<\/code> treated these as\narbitrary structures and compared them one element at a time. The PR teaches\nthe runtime that their fixed bitwise representations are suitable for\ncomparison as raw bytes. That enables the same optimized memory-comparison\npath used for primitive types, including JIT unrolling and vectorization:<\/p>\n<pre><code class=\"language-csharp\">\/\/ dotnet run -c Release -f net10.0 --filter \"*\" --runtimes net10.0 net11.0\r\n\r\nusing BenchmarkDotNet.Attributes;\r\nusing BenchmarkDotNet.Running;\r\n\r\nBenchmarkSwitcher.FromAssembly(typeof(Benchmarks).Assembly).Run(args);\r\n\r\n[HideColumns(\"Job\", \"Error\", \"StdDev\", \"Median\", \"RatioSD\")]\r\npublic class Benchmarks\r\n{\r\n    private readonly Guid[] _guids1 = new Guid[2];\r\n    private readonly Guid[] _guids2 = new Guid[2];\r\n    private readonly Int128[] _int128s1 = new Int128[2];\r\n    private readonly Int128[] _int128s2 = new Int128[2];\r\n\r\n    [Benchmark]\r\n    public bool GuidEqual() =&gt; _guids1.AsSpan().SequenceEqual(_guids2);\r\n\r\n    [Benchmark]\r\n    public bool Int128Equal() =&gt; _int128s1.AsSpan().SequenceEqual(_int128s2);\r\n}<\/code><\/pre>\n<table>\n<thead>\n<tr>\n<th>Method<\/th>\n<th>Runtime<\/th>\n<th>Mean<\/th>\n<th>Ratio<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td>GuidEqual<\/td>\n<td>.NET 10.0<\/td>\n<td>2.960 ns<\/td>\n<td>1.00<\/td>\n<\/tr>\n<tr>\n<td>GuidEqual<\/td>\n<td>.NET 11.0<\/td>\n<td>2.077 ns<\/td>\n<td>0.70<\/td>\n<\/tr>\n<tr>\n<td>Int128Equal<\/td>\n<td>.NET 10.0<\/td>\n<td>3.547 ns<\/td>\n<td>1.00<\/td>\n<\/tr>\n<tr>\n<td>Int128Equal<\/td>\n<td>.NET 11.0<\/td>\n<td>2.077 ns<\/td>\n<td>0.59<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<p>Another improvement in .NET 11 is to <code>string.Split<\/code>.\nBefore <code>string.Split<\/code> can produce the resulting strings, it first needs to\nfind the characters that separate them and record their positions. In .NET 10, that search is\nalready vectorized: rather than examine one UTF-16 character at a time, it\nloads a vector&#8217;s worth, compares all of its lanes against the separator in\nparallel, and turns the comparison result into a mask identifying any matches.\nIt then advances to the next vector, or uses the mask to record the matching\npositions. In .NET 11, on x86\/x64, <a href=\"https:\/\/github.com\/dotnet\/runtime\/pull\/125379\">dotnet\/runtime#125379<\/a>\nfrom <a href=\"https:\/\/github.com\/hamarb123\">@hamarb123<\/a> makes the no-match path cheaper\nfor ASCII separators. It loads two vectors of UTF-16 characters, packs their\n16-bit elements into one vector of bytes, and checks that combined vector for\nthe separator. If there isn&#8217;t a match, it has skipped twice as much input with\none packed comparison; only a possible match requires the full 16-bit\ncomparisons needed to determine its exact position. (This same packing technique\nis already employed elsewhere, such as in various <code>SearchValues&lt;T&gt;<\/code> implementations.)<\/p>\n<pre><code class=\"language-csharp\">\/\/ dotnet run -c Release -f net10.0 --filter \"*\" --runtimes net10.0 net11.0\r\n\r\nusing BenchmarkDotNet.Attributes;\r\nusing BenchmarkDotNet.Running;\r\n\r\nBenchmarkSwitcher.FromAssembly(typeof(Benchmarks).Assembly).Run(args);\r\n\r\n[HideColumns(\"Job\", \"Error\", \"StdDev\", \"Median\", \"RatioSD\")]\r\npublic class Benchmarks\r\n{\r\n    private readonly string _input = new('a', 16_384);\r\n\r\n    [Benchmark]\r\n    public int SplitNoSeparators() =&gt; _input.Split(',').Length;\r\n}<\/code><\/pre>\n<table>\n<thead>\n<tr>\n<th>Method<\/th>\n<th>Runtime<\/th>\n<th style=\"text-align: right;\">Mean<\/th>\n<th style=\"text-align: right;\">Ratio<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td>SplitNoSeparators<\/td>\n<td>.NET 10.0<\/td>\n<td style=\"text-align: right;\">786.0 ns<\/td>\n<td style=\"text-align: right;\">1.00<\/td>\n<\/tr>\n<tr>\n<td>SplitNoSeparators<\/td>\n<td>.NET 11.0<\/td>\n<td style=\"text-align: right;\">404.7 ns<\/td>\n<td style=\"text-align: right;\">0.51<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<p>A related Arm64 text-search improvement comes from\n<a href=\"https:\/\/github.com\/dotnet\/runtime\/pull\/126678\">dotnet\/runtime#126678<\/a>.\nA vector comparison produces a vector whose elements are all zero for\nnon-matches and all one bits for matches. Finding the first or last match then\nrequires condensing those bits into a scalar value and counting its leading or\ntrailing zeros. On x86, the runtime can use a movemask instruction for that\ncondensing step. Arm64 has no direct equivalent, and the old implementation\nneeded a sequence of shifts, widening operations, and a horizontal add to achieve it.\nThe .NET 11 implementation now uses <code>shrn<\/code>, Arm64&#8217;s shift-right-and-narrow\ninstruction, to pack the relevant bits directly. <code>SearchValues&lt;char&gt;<\/code> uses these helpers, so the following benchmark reaches\nthe affected code while searching for a match at the end of the input.<\/p>\n<pre><code class=\"language-csharp\">\/\/ dotnet run -c Release -f net10.0 --filter \"*\" --runtimes net10.0 net11.0\r\n\r\nusing BenchmarkDotNet.Attributes;\r\nusing BenchmarkDotNet.Running;\r\nusing System.Buffers;\r\n\r\nBenchmarkSwitcher.FromAssembly(typeof(Benchmarks).Assembly).Run(args);\r\n\r\n[DisassemblyDiagnoser(maxDepth: 3)]\r\n[HideColumns(\"Job\", \"Error\", \"StdDev\", \"Median\", \"RatioSD\")]\r\npublic class Benchmarks\r\n{\r\n    private const int Length = 8_192;\r\n    private static readonly SearchValues&lt;char&gt; s_vowels = SearchValues.Create(\"aeiouAEIOU\");\r\n    private static readonly string s_input = new string('x', Length - 1) + 'e';\r\n\r\n    [Benchmark]\r\n    public int IndexOfAny() =&gt; s_input.AsSpan().IndexOfAny(s_vowels);\r\n}<\/code><\/pre>\n<p>The .NET 10 match-index path requires this sequence:<\/p>\n<pre><code class=\"language-armasm\">; Arm64\r\n; .NET 10\r\ncmeq    v16.16b, v16.16b, #0\r\nmovi    v17.16b, #0x80\r\nand     v16.16b, v16.16b, v17.16b\r\nldr     q17, [MASK]\r\nushl    v16.16b, v16.16b, v17.16b\r\nuxtl2   v17.8h, v16.16b\r\nshl     v17.8h, v17.8h, #8\r\nuaddw   v16.8h, v17.8h, v16.8b\r\naddv    h16, v16.8h\r\numov    w2, v16.h[0]\r\nmvn     w2, w2\r\nrbit    w2, w2\r\nclz     w2, w2<\/code><\/pre>\n<p>In .NET 11, the equivalent work is simpler:<\/p>\n<pre><code class=\"language-armasm\">; Arm64\r\n; .NET 11\r\ncmeq    v16.16b, v16.16b, #0\r\nmvn     v16.16b, v16.16b\r\nshrn    v16.8b, v16.8h, #4\r\nfmov    x2, d16\r\nrbit    x2, x2\r\nclz     x2, x2\r\nlsr     w2, w2, #2<\/code><\/pre>\n<p><code>MemoryExtensions<\/code> already provides span-based searches for one or more values\nwith <code>IndexOfAny<\/code>, and for contiguous ranges with <code>IndexOfAnyInRange<\/code>, along\nwith <code>Except<\/code>, <code>Contains<\/code>, and last-index variants of these operations. For\nexample, <code>span.IndexOfAnyInRange('0', '9')<\/code> finds the next ASCII digit.\nWhitespace is also common to search for, but the characters recognized by\n<code>char.IsWhiteSpace<\/code> are spread across multiple parts of Unicode rather than\nforming one contiguous range. To avoid requiring every caller to construct\nthe same <code>SearchValues&lt;char&gt;<\/code>,\n<a href=\"https:\/\/github.com\/dotnet\/runtime\/pull\/111439\">dotnet\/runtime#111439<\/a> from\n<a href=\"https:\/\/github.com\/AlexRadch\">@AlexRadch<\/a> adds\n<code>ContainsAnyWhiteSpace<\/code>, <code>IndexOfAnyWhiteSpace<\/code>,\n<code>IndexOfAnyExceptWhiteSpace<\/code>, <code>LastIndexOfAnyWhiteSpace<\/code>, and\n<code>LastIndexOfAnyExceptWhiteSpace<\/code> for <code>ReadOnlySpan&lt;char&gt;<\/code>. Their shared\n<code>SearchValues&lt;char&gt;<\/code>-based implementation vectorizes these searches for\nparsers, validators, trimming code, and other text-processing code.<\/p>\n<p>This is, however, a good example of how vectorization isn&#8217;t always a win. Take\ntrimming. To trim leading whitespace, code needs to find the first character\nthat isn&#8217;t whitespace. That character could be deep into the string, but in\nthe most common case, there&#8217;s little or nothing to trim. A scalar loop can\nthen return after inspecting just one or two characters, whereas the\nvectorized helper has fixed setup cost. It&#8217;s still worth vectorizing, because\nthat overhead is small and the benefits when there is a lot to scan can be\nsignificant. Something to keep in mind.<\/p>\n<pre><code class=\"language-csharp\">\/\/ dotnet run -c Release -f net11.0 --filter \"*\"\r\n\r\nusing BenchmarkDotNet.Attributes;\r\nusing BenchmarkDotNet.Running;\r\n\r\nBenchmarkSwitcher.FromAssembly(typeof(Benchmarks).Assembly).Run(args);\r\n\r\n[HideColumns(\"Job\", \"Error\", \"StdDev\", \"Median\", \"RatioSD\")]\r\npublic class Benchmarks\r\n{\r\n    private readonly string _input = new(' ', 256);\r\n\r\n    [Benchmark(Baseline = true)]\r\n    public int Scalar()\r\n    {\r\n        ReadOnlySpan&lt;char&gt; input = _input;\r\n        for (int i = 0; i &lt; input.Length; i++)\r\n        {\r\n            if (!char.IsWhiteSpace(input[i]))\r\n                return i;\r\n        }\r\n\r\n        return -1;\r\n    }\r\n\r\n    [Benchmark]\r\n    public int Vectorized() =&gt; _input.AsSpan().IndexOfAnyExceptWhiteSpace();\r\n}<\/code><\/pre>\n<table>\n<thead>\n<tr>\n<th>Method<\/th>\n<th>Mean<\/th>\n<th>Ratio<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td>Scalar<\/td>\n<td>127.87 ns<\/td>\n<td>1.00<\/td>\n<\/tr>\n<tr>\n<td>Vectorized<\/td>\n<td>13.14 ns<\/td>\n<td>0.10<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<p>This method is a particularly good fit when needing to validate that input does not contain any whitespace; that requires searching the entirety of input, which is where the vectorization in these methods shines. As an example of this, <a href=\"https:\/\/github.com\/dotnet\/runtime\/pull\/127123\">dotnet\/runtime#127123<\/a> uses it to accelerate the parsing of the <code>\"X\"<\/code> GUID format:<\/p>\n<pre><code class=\"language-csharp\">\/\/ dotnet run -c Release -f net10.0 --filter \"*\" --runtimes net10.0 net11.0\r\n\r\nusing BenchmarkDotNet.Attributes;\r\nusing BenchmarkDotNet.Running;\r\n\r\nBenchmarkSwitcher.FromAssembly(typeof(Benchmarks).Assembly).Run(args);\r\n\r\n[HideColumns(\"Job\", \"Error\", \"StdDev\", \"Median\", \"RatioSD\")]\r\npublic class Benchmarks\r\n{\r\n    private static readonly string s_noWhitespace =\r\n        Guid.Parse(\"a8098c1a-f86e-11da-bd1a-00112444be1e\").ToString(\"X\");\r\n\r\n    [Benchmark]\r\n    public Guid ParseExactX() =&gt; Guid.ParseExact(s_noWhitespace, \"X\");\r\n}<\/code><\/pre>\n<table>\n<thead>\n<tr>\n<th>Method<\/th>\n<th>Runtime<\/th>\n<th>Mean<\/th>\n<th>Ratio<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td>ParseExactX<\/td>\n<td>.NET 10.0<\/td>\n<td>120.6 ns<\/td>\n<td>1.00<\/td>\n<\/tr>\n<tr>\n<td>ParseExactX<\/td>\n<td>.NET 11.0<\/td>\n<td>87.10 ns<\/td>\n<td>0.72<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<p>Closely related to searching is sorting. Years ago, sorting methods for <code>Span&lt;T&gt;<\/code> were added to <code>MemoryExtensions<\/code>. Interestingly, the method wasn&#8217;t added as <code>Sort&lt;T&gt;<\/code> but rather as <code>Sort&lt;T, TComparer&gt;<\/code> where <code>TComparer : IComparer&lt;T&gt;<\/code>. That signature enables a caller to provide a struct comparer without allocating a delegate or class-based comparer. Because the comparer is a constrained value type, the JIT should also be able to inline the comparison into the hot sorting loop. In practice, the implementation boxed the struct into an <code>IComparer&lt;T&gt;<\/code>, both allocating and turning every comparison back into an interface call. This was known at the time, but avoiding the box used generic implementation techniques that then carried too much runtime and code-size cost. Those supporting costs have since been addressed, so <a href=\"https:\/\/github.com\/dotnet\/runtime\/pull\/116109\">dotnet\/runtime#116109<\/a> from <a href=\"https:\/\/github.com\/2A5F\">@2A5F<\/a> now carries a value-type comparer through <code>Span&lt;T&gt;.Sort<\/code> without boxing it. The JIT can specialize the sorting routine for that comparer and inline the comparison.<\/p>\n<p>The generic specialization does increase generated code and very large comparer structs can be more expensive to copy; this optimization is aimed at the small stateless or lightly stateful structs for which the API was designed.<\/p>\n<pre><code class=\"language-csharp\">\/\/ dotnet run -c Release -f net10.0 --filter \"*\" --runtimes net10.0 net11.0\r\n\r\nusing BenchmarkDotNet.Attributes;\r\nusing BenchmarkDotNet.Running;\r\n\r\nBenchmarkSwitcher.FromAssembly(typeof(Benchmarks).Assembly).Run(args);\r\n\r\n[MemoryDiagnoser(false), HideColumns(\"Job\", \"Error\", \"StdDev\", \"Median\", \"RatioSD\")]\r\npublic class Benchmarks\r\n{\r\n    private readonly int[] _source = Enumerable.Range(0, 512).Select(i =&gt; (i * 257) % 512).ToArray();\r\n    private int[] _values = [];\r\n\r\n    [IterationSetup]\r\n    public void Setup() =&gt; _values = (int[])_source.Clone();\r\n\r\n    [Benchmark]\r\n    public void Sort() =&gt; _values.AsSpan().Sort(new DescendingComparer());\r\n\r\n    private readonly struct DescendingComparer : IComparer&lt;int&gt;\r\n    {\r\n        public int Compare(int x, int y) =&gt; y.CompareTo(x);\r\n    }\r\n}<\/code><\/pre>\n<table>\n<thead>\n<tr>\n<th>Method<\/th>\n<th>Runtime<\/th>\n<th>Mean<\/th>\n<th>Ratio<\/th>\n<th>Allocated<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td>Sort<\/td>\n<td>.NET 10.0<\/td>\n<td>11.62 \u03bcs<\/td>\n<td>1.00<\/td>\n<td>88 B<\/td>\n<\/tr>\n<tr>\n<td>Sort<\/td>\n<td>.NET 11.0<\/td>\n<td>3.533 \u03bcs<\/td>\n<td>0.30<\/td>\n<td>&#8211;<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<h2>Collections and LINQ<\/h2>\n<p>Much of the collection and LINQ work in .NET 11 comes from taking better\nadvantage of information that&#8217;s already available. A collection often knows\nmuch more than an <code>IEnumerable&lt;T&gt;<\/code> can express: its count, its contiguous\nstorage, its comparer, or the layout of its hash table. Similarly, a LINQ\niterator can know how many elements it represents or how its operations were\ncomposed. Preserving that information can avoid enumeration, temporary\nstorage, repeated hashing, and other work a general-purpose implementation\nwould otherwise need to perform.<\/p>\n<p><a href=\"https:\/\/github.com\/dotnet\/runtime\/pull\/119896\">dotnet\/runtime#119896<\/a> from <a href=\"https:\/\/github.com\/prozolic\">@prozolic<\/a> changes <code>ImmutableArray.Create<\/code> to use <code>Array.Copy<\/code> rather than a hand-written element loop. A general element-by-element copy repeatedly performs indexing and assignment, while the runtime can specialize <code>Array.Copy<\/code> for the element type and size. For blittable data, it can use optimized bulk memory copies, and for reference types, which need GC write barriers, it performs the required write barriers in the runtime&#8217;s tuned copy helpers. The change therefore both simplifies the managed code and gives <code>ImmutableArray<\/code> access to those optimized implementations.<\/p>\n<pre><code class=\"language-csharp\">\/\/ dotnet run -c Release -f net10.0 --filter \"*\" --runtimes net10.0 net11.0\r\n\r\nusing BenchmarkDotNet.Running;\r\n\r\nusing System.Collections.Immutable;\r\nusing BenchmarkDotNet.Attributes;\r\n\r\nBenchmarkSwitcher.FromAssembly(typeof(Benchmarks).Assembly).Run(args);\r\n\r\n[HideColumns(\"Job\", \"Error\", \"StdDev\", \"RatioSD\", \"Median\")]\r\npublic class Benchmarks\r\n{\r\n    private readonly int[] _source = Enumerable.Range(0, 1_000).ToArray();\r\n\r\n    [Benchmark]\r\n    public ImmutableArray&lt;int&gt; CreateSlice() =&gt; ImmutableArray.Create(_source, 0, _source.Length);\r\n}<\/code><\/pre>\n<p>This in particular makes larger copies much faster.<\/p>\n<table>\n<thead>\n<tr>\n<th>Method<\/th>\n<th>Runtime<\/th>\n<th>Mean<\/th>\n<th>Ratio<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td>CreateSlice<\/td>\n<td>.NET 10.0<\/td>\n<td>552.5 ns<\/td>\n<td>1.00<\/td>\n<\/tr>\n<tr>\n<td>CreateSlice<\/td>\n<td>.NET 11.0<\/td>\n<td>277.7 ns<\/td>\n<td>0.50<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<p><a href=\"https:\/\/github.com\/dotnet\/runtime\/pull\/118932\">dotnet\/runtime#118932<\/a> from <a href=\"https:\/\/github.com\/prozolic\">@prozolic<\/a> similarly keeps <code>ImmutableArrayExtensions.SequenceEqual<\/code> on optimized paths when the other sequence is an array, list, or another <code>ICollection&lt;T&gt;<\/code>.<\/p>\n<pre><code class=\"language-csharp\">\/\/ dotnet run -c Release -f net10.0 --filter \"*\" --runtimes net10.0 net11.0\r\n\r\nusing BenchmarkDotNet.Running;\r\n\r\nusing System.Collections.Immutable;\r\nusing BenchmarkDotNet.Attributes;\r\n\r\nBenchmarkSwitcher.FromAssembly(typeof(Benchmarks).Assembly).Run(args);\r\n\r\n[HideColumns(\"Job\", \"Error\", \"StdDev\", \"RatioSD\", \"Median\")]\r\npublic class Benchmarks\r\n{\r\n    private ImmutableArray&lt;int&gt; _immutable;\r\n    private List&lt;int&gt; _list = [];\r\n\r\n    [GlobalSetup]\r\n    public void Setup()\r\n    {\r\n        int[] values = Enumerable.Range(0, 1_000).ToArray();\r\n        _immutable = ImmutableArray.Create(values);\r\n        _list = [.. values];\r\n    }\r\n\r\n    [Benchmark]\r\n    public bool SequenceEqual() =&gt; _immutable.SequenceEqual(_list);\r\n}<\/code><\/pre>\n<table>\n<thead>\n<tr>\n<th>Method<\/th>\n<th>Runtime<\/th>\n<th>Mean<\/th>\n<th>Ratio<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td>SequenceEqual<\/td>\n<td>.NET 10.0<\/td>\n<td>925.8 ns<\/td>\n<td>1.00<\/td>\n<\/tr>\n<tr>\n<td>SequenceEqual<\/td>\n<td>.NET 11.0<\/td>\n<td>122.2 ns<\/td>\n<td>0.13<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<p><code>Array.FindAll<\/code> has the opposite job: it produces a new collection. For a small result, its temporary storage used to cost more than the result itself.\n<a href=\"https:\/\/github.com\/dotnet\/runtime\/pull\/120336\">dotnet\/runtime#120336<\/a> from <a href=\"https:\/\/github.com\/Henr1k80\">@Henr1k80<\/a> has <code>Array.FindAll<\/code> collect its first four matches in an inline stack buffer rather than an intermediate <code>List&lt;T&gt;<\/code>:<\/p>\n<pre><code class=\"language-csharp\">\/\/ dotnet run -c Release -f net10.0 --filter \"*\" --runtimes net10.0 net11.0\r\n\r\nusing BenchmarkDotNet.Running;\r\n\r\nusing BenchmarkDotNet.Attributes;\r\n\r\nBenchmarkSwitcher.FromAssembly(typeof(Benchmarks).Assembly).Run(args);\r\n\r\n[MemoryDiagnoser(false), HideColumns(\"Job\", \"Error\", \"StdDev\", \"RatioSD\", \"Median\")]\r\npublic class Benchmarks\r\n{\r\n    private int[] _data = [];\r\n\r\n    [Params(4, 5)]\r\n    public int Size { get; set; }\r\n\r\n    [GlobalSetup]\r\n    public void Setup() =&gt; _data = Enumerable.Range(0, Size).ToArray();\r\n\r\n    [Benchmark]\r\n    public int[] FindAllMatch() =&gt; Array.FindAll(_data, static _ =&gt; true);\r\n}<\/code><\/pre>\n<table>\n<thead>\n<tr>\n<th>Method<\/th>\n<th>Runtime<\/th>\n<th>Size<\/th>\n<th>Mean<\/th>\n<th>Ratio<\/th>\n<th>Allocated<\/th>\n<th>Alloc Ratio<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td>FindAllMatch<\/td>\n<td>.NET 10.0<\/td>\n<td>4<\/td>\n<td>27.61 ns<\/td>\n<td>1.00<\/td>\n<td>112 B<\/td>\n<td>1.00<\/td>\n<\/tr>\n<tr>\n<td>FindAllMatch<\/td>\n<td>.NET 11.0<\/td>\n<td>4<\/td>\n<td>9.212 ns<\/td>\n<td>0.33<\/td>\n<td>40 B<\/td>\n<td>0.36<\/td>\n<\/tr>\n<tr>\n<td>FindAllMatch<\/td>\n<td>.NET 10.0<\/td>\n<td>5<\/td>\n<td>36.93 ns<\/td>\n<td>1.00<\/td>\n<td>176 B<\/td>\n<td>1.00<\/td>\n<\/tr>\n<tr>\n<td>FindAllMatch<\/td>\n<td>.NET 11.0<\/td>\n<td>5<\/td>\n<td>11.102 ns<\/td>\n<td>0.30<\/td>\n<td>48 B<\/td>\n<td>0.27<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<p><code>Dictionary&lt;TKey, TValue&gt;.Remove<\/code> had also missed an optimization already used by lookup and insertion. <a href=\"https:\/\/github.com\/dotnet\/runtime\/pull\/125884\">dotnet\/runtime#125884<\/a> gives value-type keys a streamlined loop for the common default-comparer case. Because that path doesn&#8217;t need a virtual comparer call, the JIT can keep more of the operation&#8217;s state in registers; reference-type keys and custom comparers continue to use the general path.<\/p>\n<pre><code class=\"language-csharp\">\/\/ dotnet run -c Release -f net10.0 --filter \"*\" --runtimes net10.0 net11.0\r\n\r\nusing BenchmarkDotNet.Attributes;\r\nusing BenchmarkDotNet.Running;\r\n\r\nBenchmarkSwitcher.FromAssembly(typeof(Benchmarks).Assembly).Run(args);\r\n\r\n[HideColumns(\"Job\", \"Error\", \"StdDev\", \"Median\", \"RatioSD\")]\r\npublic class Benchmarks\r\n{\r\n    private readonly Guid[] _keys = Enumerable.Range(0, 512).Select(i =&gt; new Guid(i, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0)).ToArray();\r\n    private Dictionary&lt;Guid, int&gt; _dictionary = [];\r\n\r\n    [IterationSetup]\r\n    public void Setup() =&gt; _dictionary = _keys.ToDictionary(key =&gt; key, key =&gt; key.GetHashCode());\r\n\r\n    [Benchmark(OperationsPerInvoke = 512)]\r\n    public void Remove()\r\n    {\r\n        foreach (Guid key in _keys)\r\n            _dictionary.Remove(key);\r\n    }\r\n}<\/code><\/pre>\n<table>\n<thead>\n<tr>\n<th>Method<\/th>\n<th>Runtime<\/th>\n<th>Mean<\/th>\n<th>Ratio<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td>Remove<\/td>\n<td>.NET 10.0<\/td>\n<td>5.285 ns<\/td>\n<td>1.00<\/td>\n<\/tr>\n<tr>\n<td>Remove<\/td>\n<td>.NET 11.0<\/td>\n<td>4.321 ns<\/td>\n<td>0.82<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<p><a href=\"https:\/\/github.com\/dotnet\/runtime\/pull\/125893\">dotnet\/runtime#125893<\/a> changes <code>HashSet&lt;T&gt;<\/code>&#8216;s internal chain walks to test the entry index against the array length with an unsigned comparison. That proves the subsequent array access is in range, allowing the JIT to remove its bounds check.<\/p>\n<pre><code class=\"language-csharp\">\/\/ dotnet run -c Release -f net10.0 --filter \"*\" --runtimes net10.0 net11.0\r\n\r\nusing BenchmarkDotNet.Attributes;\r\nusing BenchmarkDotNet.Running;\r\n\r\nBenchmarkSwitcher.FromAssembly(typeof(Benchmarks).Assembly).Run(args);\r\n\r\n[HideColumns(\"Job\", \"Error\", \"StdDev\", \"Median\", \"RatioSD\")]\r\npublic class Benchmarks\r\n{\r\n    private readonly HashSet&lt;int&gt; _set = Enumerable.Range(0, 4096).ToHashSet();\r\n    private readonly int[] _probes = Enumerable.Range(0, 4096).ToArray();\r\n\r\n    [Benchmark]\r\n    public int ContainsHits()\r\n    {\r\n        int count = 0;\r\n        foreach (int value in _probes)\r\n            count += _set.Contains(value) ? 1 : 0;\r\n\r\n        return count;\r\n    }\r\n}<\/code><\/pre>\n<table>\n<thead>\n<tr>\n<th>Method<\/th>\n<th>Runtime<\/th>\n<th>Mean<\/th>\n<th>Ratio<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td>ContainsHits<\/td>\n<td>.NET 10.0<\/td>\n<td>7.870 \u03bcs<\/td>\n<td>1.00<\/td>\n<\/tr>\n<tr>\n<td>ContainsHits<\/td>\n<td>.NET 11.0<\/td>\n<td>7.388 \u03bcs<\/td>\n<td>0.94<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<p><a href=\"https:\/\/github.com\/dotnet\/runtime\/pull\/128988\">dotnet\/runtime#128988<\/a> from <a href=\"https:\/\/github.com\/prozolic\">@prozolic<\/a> removes a second hash-table lookup when removing a matching key-value pair from <code>OrderedDictionary&lt;TKey, TValue&gt;<\/code> through <code>ICollection&lt;KeyValuePair&lt;TKey, TValue&gt;&gt;<\/code>. That interface operation must first find the key and verify that its stored value equals the supplied value. Once both checks have succeeded, the implementation already has the entry index needed for removal. Looking up the key again unnecessarily repeats its hash computation and collision-chain walk, so the updated path removes the known entry directly.<\/p>\n<pre><code class=\"language-csharp\">\/\/ dotnet run -c Release -f net10.0 --filter \"*\" --runtimes net10.0 net11.0\r\n\r\nusing BenchmarkDotNet.Running;\r\n\r\nusing System.Collections.Generic;\r\nusing BenchmarkDotNet.Attributes;\r\n\r\nBenchmarkSwitcher.FromAssembly(typeof(Benchmarks).Assembly).Run(args);\r\n\r\n[HideColumns(\"Job\", \"Error\", \"StdDev\", \"RatioSD\", \"Median\")]\r\npublic class Benchmarks\r\n{\r\n    private const int N = 10_000;\r\n\r\n    private OrderedDictionary&lt;string, int&gt; _dict = [];\r\n    private KeyValuePair&lt;string, int&gt;[] _pairs = Enumerable.Range(0, N)\r\n        .Select(i =&gt; new KeyValuePair&lt;string, int&gt;($\"key{i}\", i))\r\n        .ToArray();\r\n\r\n    [IterationSetup]\r\n    public void IterationSetup() =&gt; _dict = new OrderedDictionary&lt;string, int&gt;(_pairs);\r\n\r\n    [Benchmark]\r\n    public int Remove_ExplicitInterface()\r\n    {\r\n        ICollection&lt;KeyValuePair&lt;string, int&gt;&gt; col = _dict;\r\n        int removed = 0;\r\n        foreach (var pair in _pairs)\r\n            if (col.Remove(pair))\r\n                removed++;\r\n\r\n        return removed;\r\n    }\r\n}<\/code><\/pre>\n<p>For 10,000 entries:<\/p>\n<table>\n<thead>\n<tr>\n<th>Method<\/th>\n<th>Runtime<\/th>\n<th>Mean<\/th>\n<th>Ratio<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td>Remove_ExplicitInterface<\/td>\n<td>.NET 10.0<\/td>\n<td>220.7 ms<\/td>\n<td>1.00<\/td>\n<\/tr>\n<tr>\n<td>Remove_ExplicitInterface<\/td>\n<td>.NET 11.0<\/td>\n<td>179.0 ms<\/td>\n<td>0.81<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<p><a href=\"https:\/\/github.com\/dotnet\/runtime\/pull\/122952\">dotnet\/runtime#122952<\/a> goes further when two hash tables have compatible layouts. Normally, <code>UnionWith<\/code> enumerates the source and inserts every element independently, recomputing hashes, checking for duplicates, and potentially resizing the destination along the way. If the destination is empty and both sets use compatible comparers, every source entry is already unique under exactly the equality rules the destination needs. <code>UnionWith<\/code> can therefore use the existing <code>HashSet&lt;T&gt;<\/code> copy-constructor fast path to clone the populated storage rather than rebuilding the same table entry by entry.<\/p>\n<pre><code class=\"language-csharp\">\/\/ dotnet run -c Release -f net10.0 --filter \"*\" --runtimes net10.0 net11.0\r\n\r\nusing BenchmarkDotNet.Running;\r\n\r\nusing System.Collections.Generic;\r\nusing System.Linq;\r\nusing BenchmarkDotNet.Attributes;\r\n\r\nBenchmarkSwitcher.FromAssembly(typeof(Benchmarks).Assembly).Run(args);\r\n\r\n[MemoryDiagnoser(false), HideColumns(\"Job\", \"Error\", \"StdDev\", \"RatioSD\", \"Median\")]\r\npublic class Benchmarks\r\n{\r\n    private readonly HashSet&lt;int&gt; _source = new(Enumerable.Range(0, 4_096));\r\n\r\n    [Benchmark]\r\n    public HashSet&lt;int&gt; FreshDestinationUnionWith()\r\n    {\r\n        HashSet&lt;int&gt; destination = [];\r\n        destination.UnionWith(_source);\r\n        return destination;\r\n    }\r\n}<\/code><\/pre>\n<table>\n<thead>\n<tr>\n<th>Method<\/th>\n<th>Runtime<\/th>\n<th style=\"text-align: right;\">Mean<\/th>\n<th style=\"text-align: right;\">Ratio<\/th>\n<th style=\"text-align: right;\">Allocated<\/th>\n<th style=\"text-align: right;\">Alloc Ratio<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td>FreshDestinationUnionWith<\/td>\n<td>.NET 10.0<\/td>\n<td style=\"text-align: right;\">46.207 \u03bcs<\/td>\n<td style=\"text-align: right;\">1.00<\/td>\n<td style=\"text-align: right;\">252.27 KB<\/td>\n<td style=\"text-align: right;\">1.00<\/td>\n<\/tr>\n<tr>\n<td>FreshDestinationUnionWith<\/td>\n<td>.NET 11.0<\/td>\n<td style=\"text-align: right;\">2.433 \u03bcs<\/td>\n<td style=\"text-align: right;\">0.05<\/td>\n<td style=\"text-align: right;\">76.07 KB<\/td>\n<td style=\"text-align: right;\">0.30<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<p><a href=\"https:\/\/github.com\/dotnet\/runtime\/pull\/128300\">dotnet\/runtime#128300<\/a>\nfrom <a href=\"https:\/\/github.com\/AndrewP-GH\">@AndrewP-GH<\/a> also helps with collection construction. Building a <code>FrozenDictionary&lt;TKey, TValue&gt;<\/code> first requires collecting the input elements into a regular <code>Dictionary&lt;TKey, TValue&gt;<\/code> if they&#8217;re not already in one. That temporary dictionary resolves duplicate keys before the final frozen representation is chosen, but in .NET 10 it was growing incrementally even when the source&#8217;s count was readily available. This PR uses that count as the\ndictionary&#8217;s initial capacity, avoiding repeated allocation, copying, and\nrehashing as it&#8217;s populated.<\/p>\n<pre><code class=\"language-csharp\">\/\/ dotnet run -c Release -f net10.0 --filter \"*\" --runtimes net10.0 net11.0\r\nusing BenchmarkDotNet.Attributes;\r\nusing BenchmarkDotNet.Running;\r\nusing System.Collections.Concurrent;\r\nusing System.Collections.Frozen;\r\nusing System.Collections.Generic;\r\nusing System.Collections.Immutable;\r\nusing System.Linq;\r\n\r\nBenchmarkSwitcher.FromAssembly(typeof(Benchmarks).Assembly).Run(args);\r\n\r\n[MemoryDiagnoser(false), HideColumns(\"Job\", \"Error\", \"StdDev\", \"Median\", \"RatioSD\")]\r\npublic class Benchmarks\r\n{\r\n    private readonly KeyValuePair&lt;int, int&gt;[] _array =\r\n        Enumerable.Range(0, 4096).Select(i =&gt; new KeyValuePair&lt;int, int&gt;(i, i)).ToArray();\r\n\r\n    [Benchmark]\r\n    public FrozenDictionary&lt;int, int&gt; FromArray() =&gt; _array.ToFrozenDictionary();\r\n\r\n}<\/code><\/pre>\n<table>\n<thead>\n<tr>\n<th>Method<\/th>\n<th>Runtime<\/th>\n<th>Allocated<\/th>\n<th>Alloc Ratio<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td>FromArray<\/td>\n<td>.NET 10.0<\/td>\n<td>347.17 KB<\/td>\n<td>1.00<\/td>\n<\/tr>\n<tr>\n<td>FromArray<\/td>\n<td>.NET 11.0<\/td>\n<td>127.16 KB<\/td>\n<td>0.37<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<p><code>SetEquals<\/code> asks whether two sets contain the same values, regardless of insertion order. The general implementation needs a temporary mutable set so it can account for duplicates and arbitrary enumeration order. When the other input is already a hash set with a compatible comparer, though, that reconstruction is unnecessary. <a href=\"https:\/\/github.com\/dotnet\/runtime\/pull\/126309\">dotnet\/runtime#126309<\/a> from <a href=\"https:\/\/github.com\/aw0lid\">@aw0lid<\/a> adds to <code>ImmutableHashSet&lt;T&gt;.SetEquals<\/code> direct zero-allocation paths for compatible <code>ImmutableHashSet&lt;T&gt;<\/code> and <code>HashSet&lt;T&gt;<\/code> inputs; with an identical comparer, the sets can be considered equal if they have the same count and if every element from one is found in the other.<\/p>\n<pre><code class=\"language-csharp\">\/\/ dotnet run -c Release -f net10.0 --filter \"*\" --runtimes net10.0 net11.0\r\n\r\nusing System.Collections.Immutable;\r\nusing BenchmarkDotNet.Attributes;\r\nusing BenchmarkDotNet.Running;\r\n\r\nBenchmarkSwitcher.FromAssembly(typeof(Benchmarks).Assembly).Run(args);\r\n\r\n[MemoryDiagnoser(false)]\r\n[HideColumns(\"Job\", \"Error\", \"StdDev\", \"Median\", \"RatioSD\")]\r\npublic class Benchmarks\r\n{\r\n    private ImmutableHashSet&lt;int&gt; _set = ImmutableHashSet&lt;int&gt;.Empty;\r\n    private ImmutableHashSet&lt;int&gt; _immutable = ImmutableHashSet&lt;int&gt;.Empty;\r\n    private HashSet&lt;int&gt; _mutable = [];\r\n\r\n    [GlobalSetup]\r\n    public void Setup()\r\n    {\r\n        int[] items = Enumerable.Range(0, 10_000).ToArray();\r\n        _set = ImmutableHashSet.CreateRange(items);\r\n        _immutable = ImmutableHashSet.CreateRange(items);\r\n        _mutable = new(items);\r\n    }\r\n\r\n    [Benchmark]\r\n    public bool EqualImmutableHashSet() =&gt; _set.SetEquals(_immutable);\r\n\r\n    [Benchmark]\r\n    public bool EqualHashSet() =&gt; _set.SetEquals(_mutable);\r\n}<\/code><\/pre>\n<table>\n<thead>\n<tr>\n<th>Method<\/th>\n<th>Runtime<\/th>\n<th>Mean<\/th>\n<th>Ratio<\/th>\n<th>Allocated<\/th>\n<th>Alloc Ratio<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td>EqualImmutableHashSet<\/td>\n<td>.NET 10.0<\/td>\n<td>775.6 \u03bcs<\/td>\n<td>1.00<\/td>\n<td>158.16 KB<\/td>\n<td>1.00<\/td>\n<\/tr>\n<tr>\n<td>EqualImmutableHashSet<\/td>\n<td>.NET 11.0<\/td>\n<td>559.7 \u03bcs<\/td>\n<td>0.72<\/td>\n<td>&#8211;<\/td>\n<td>0<\/td>\n<\/tr>\n<tr>\n<td>EqualHashSet<\/td>\n<td>.NET 10.0<\/td>\n<td>478.6 \u03bcs<\/td>\n<td>1.00<\/td>\n<td>157.99 KB<\/td>\n<td>1.00<\/td>\n<\/tr>\n<tr>\n<td>EqualHashSet<\/td>\n<td>.NET 11.0<\/td>\n<td>230.9 \u03bcs<\/td>\n<td>0.48<\/td>\n<td>&#8211;<\/td>\n<td>0<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<p>Sorted sets have a related case. <code>SetEquals<\/code> can be passed any <code>IEnumerable&lt;T&gt;<\/code>. That sequence might be unordered and might contain duplicate values, so <code>ImmutableSortedSet&lt;T&gt;<\/code> previously copied it into a temporary <code>SortedSet&lt;T&gt;<\/code> before performing the comparison. However, when the input is another sorted set using the same ordering comparer, both sets contain unique values and enumerate those values\nin the same order. Equality can then be determined by first comparing their\ncounts and, if those match, advancing both enumerators together. The first\nunequal pair proves the sets are different, and reaching the end without finding\na difference proves they&#8217;re equal. <a href=\"https:\/\/github.com\/dotnet\/runtime\/pull\/126549\">dotnet\/runtime#126549<\/a> from\n<a href=\"https:\/\/github.com\/aw0lid\">@aw0lid<\/a> recognizes this case for\n<code>ImmutableSortedSet&lt;T&gt;<\/code>, avoiding the temporary <code>SortedSet&lt;T&gt;<\/code> and comparing the two sorted sequences directly in one linear pass:<\/p>\n<pre><code class=\"language-csharp\">\/\/ dotnet run -c Release -f net10.0 --filter \"*\" --runtimes net10.0 net11.0\r\n\r\nusing BenchmarkDotNet.Running;\r\n\r\nusing System.Collections.Generic;\r\nusing System.Collections.Immutable;\r\nusing BenchmarkDotNet.Attributes;\r\n\r\nBenchmarkSwitcher.FromAssembly(typeof(Benchmarks).Assembly).Run(args);\r\n\r\n[MemoryDiagnoser(false), HideColumns(\"Job\", \"Error\", \"StdDev\", \"RatioSD\", \"Median\")]\r\npublic class Benchmarks\r\n{\r\n    private const int N = 10_000;\r\n    private ImmutableSortedSet&lt;int&gt; _set = ImmutableSortedSet&lt;int&gt;.Empty;\r\n    private ImmutableSortedSet&lt;int&gt; _equalSet = ImmutableSortedSet&lt;int&gt;.Empty;\r\n\r\n    [GlobalSetup]\r\n    public void Setup()\r\n    {\r\n        var items = new int[N];\r\n        for (int i = 0; i &lt; N; i++) items[i] = i;\r\n        _set = ImmutableSortedSet.CreateRange(items);\r\n        _equalSet = ImmutableSortedSet.CreateRange(items);\r\n    }\r\n\r\n    [Benchmark]\r\n    public bool SetEquals_EqualImmutableSortedSet() =&gt; _set.SetEquals(_equalSet);\r\n}<\/code><\/pre>\n<p>For 10,000 elements:<\/p>\n<table>\n<thead>\n<tr>\n<th>Method<\/th>\n<th>Runtime<\/th>\n<th>Mean<\/th>\n<th>Ratio<\/th>\n<th>Allocated<\/th>\n<th>Alloc Ratio<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td>SetEquals_EqualImmutableSortedSet<\/td>\n<td>.NET 10.0<\/td>\n<td>767.7 \u03bcs<\/td>\n<td>1.00<\/td>\n<td>430.02 KB<\/td>\n<td>1.00<\/td>\n<\/tr>\n<tr>\n<td>SetEquals_EqualImmutableSortedSet<\/td>\n<td>.NET 11.0<\/td>\n<td>117.6 \u03bcs<\/td>\n<td>0.15<\/td>\n<td>&#8211;<\/td>\n<td>0<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<p><code>SortedSet&lt;T&gt;<\/code> already enjoyed an optimization for that case in .NET 10, but it&#8217;s not left out of .NET 11 improvements. <code>SortedSet&lt;T&gt;.GetViewBetween<\/code> returns a <code>SortedSet&lt;T&gt;<\/code> view, effectively a slice of another <code>SortedSet&lt;T&gt;<\/code>, a live window onto a range of another set: changes through the view affect the original set. Clearing a view therefore can&#8217;t replace the view with an empty collection; it must find and remove every original node in that range. <a href=\"https:\/\/github.com\/dotnet\/runtime\/pull\/126410\">dotnet\/runtime#126410<\/a> from <a href=\"https:\/\/github.com\/prozolic\">@prozolic<\/a> reduces the temporary storage used for that operation. The implementation pre-sizes the list of elements to remove and walks it by index rather than repeatedly removing from and shrinking the temporary list.<\/p>\n<pre><code class=\"language-csharp\">\/\/ dotnet run -c Release -f net10.0 --filter \"*\" --runtimes net10.0 net11.0\r\n\r\nusing BenchmarkDotNet.Running;\r\n\r\nusing BenchmarkDotNet.Attributes;\r\n\r\nBenchmarkSwitcher.FromAssembly(typeof(Benchmarks).Assembly).Run(args);\r\n\r\n[MemoryDiagnoser(false), HideColumns(\"Job\", \"Error\", \"StdDev\", \"RatioSD\", \"Median\")]\r\npublic class Benchmarks\r\n{\r\n    private const int N = 10_000;\r\n    private SortedSet&lt;int&gt; _fullSet = [];\r\n\r\n    [IterationSetup]\r\n    public void Setup() =&gt; _fullSet = new SortedSet&lt;int&gt;(Enumerable.Range(0, N));\r\n\r\n    [Benchmark]\r\n    public int GetViewBetweenThenClear()\r\n    {\r\n        SortedSet&lt;int&gt; view = _fullSet.GetViewBetween(0, N - 1);\r\n        view.Clear();\r\n        return _fullSet.Count;\r\n    }\r\n}<\/code><\/pre>\n<table>\n<thead>\n<tr>\n<th>Method<\/th>\n<th>Runtime<\/th>\n<th>Allocated<\/th>\n<th>Alloc Ratio<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td>GetViewBetweenThenClear<\/td>\n<td>.NET 10.0<\/td>\n<td>193.15 KB<\/td>\n<td>1.00<\/td>\n<\/tr>\n<tr>\n<td>GetViewBetweenThenClear<\/td>\n<td>.NET 11.0<\/td>\n<td>103.93 KB<\/td>\n<td>0.54<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<p>Collections are frequently consumed through LINQ. Although its operators work\nin terms of the general <code>IEnumerable&lt;T&gt;<\/code> abstraction, LINQ&#8217;s internal\niterators can preserve useful facts about their sources and the operations\nalready applied. Those facts can sometimes answer a query without enumerating\nthe source at all.<\/p>\n<p>For example, consider <code>source.Append(x).Skip(10).LastOrDefault()<\/code>. LINQ queries are lazy, so the actual search begins only when <code>LastOrDefault<\/code> asks the <code>Skip<\/code> iterator for its last element. If <code>source.Append(x)<\/code> contains ten or fewer elements,\n<code>Skip(10)<\/code> necessarily removes all of them, leaving an empty sequence from\nwhich <code>LastOrDefault<\/code> must return the default value. <code>Append<\/code>, <code>Prepend<\/code>, and\n<code>Concat<\/code> iterators can cheaply report their total count when their underlying\nsources can do so. <a href=\"https:\/\/github.com\/dotnet\/runtime\/pull\/123306\">dotnet\/runtime#123306<\/a> from <a href=\"https:\/\/github.com\/prozolic\">@prozolic<\/a> teaches the last-element path for <code>Skip<\/code> to compare that count with the number being skipped and immediately\nreport that there is no element, rather than searching a sequence it already\nknows is empty.<\/p>\n<pre><code class=\"language-csharp\">\/\/ dotnet run -c Release -f net10.0 --filter \"*\" --runtimes net10.0 net11.0\r\n\r\nusing BenchmarkDotNet.Running;\r\n\r\nusing System.Linq;\r\nusing BenchmarkDotNet.Attributes;\r\n\r\nBenchmarkSwitcher.FromAssembly(typeof(Benchmarks).Assembly).Run(args);\r\n\r\n[MemoryDiagnoser(false), HideColumns(\"Job\", \"Error\", \"StdDev\", \"RatioSD\", \"Median\")]\r\npublic class Benchmarks\r\n{\r\n    private readonly int[] _source = [1, 2, 3, 4, 5];\r\n\r\n    [Benchmark]\r\n    public int AppendSkipLastOrDefault() =&gt; _source.Append(6).Skip(10).LastOrDefault();\r\n}<\/code><\/pre>\n<table>\n<thead>\n<tr>\n<th>Method<\/th>\n<th>Runtime<\/th>\n<th>Mean<\/th>\n<th>Ratio<\/th>\n<th>Allocated<\/th>\n<th>Alloc Ratio<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td>AppendSkipLastOrDefault<\/td>\n<td>.NET 10.0<\/td>\n<td>44.89 ns<\/td>\n<td>1.00<\/td>\n<td>144 B<\/td>\n<td>1.00<\/td>\n<\/tr>\n<tr>\n<td>AppendSkipLastOrDefault<\/td>\n<td>.NET 11.0<\/td>\n<td>16.32 ns<\/td>\n<td>0.36<\/td>\n<td>112 B<\/td>\n<td>0.78<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<p>.NET 11 also improve&#8217;s LINQ&#8217;s <code>Enumerable.Sum<\/code>. <code>Sum<\/code> already uses SIMD. The\nmain loop processes four vectors at a time, alternating between two\naccumulators so that the additions don&#8217;t require extra moves. However, <code>Sum<\/code>\nalso promises to throw if the result overflows. Alongside each vector addition,\nthe implementation uses the signs of the two inputs and the result to update\nanother vector that tracks whether any lane overflowed. After every\ngroup of four vectors, the loop tests that tracking vector and branches to the\nthrowing path if needed. Overflow is rare, though, so on the common path that test and branch almost always just\nconfirm that nothing happened. <a href=\"https:\/\/github.com\/dotnet\/runtime\/pull\/127429\">dotnet\/runtime#127429<\/a>\nremoves that repeated work in .NET 11. It accumulates the overflow information\nacross all of the vector processing and tests it once after the vector loops\nhave completed. The checked-overflow behavior remains the same, but the normal\npath no longer needs to stop and check after every four vectors. The PR also\nsimplifies how the method walks the input, replacing unsafe reference and index\narithmetic with span-based vector loads, progressively slicing off the elements\nalready processed, and using a <code>foreach<\/code> for the final scalar elements.<\/p>\n<pre><code class=\"language-csharp\">\/\/ dotnet run -c Release -f net10.0 --filter \"*\" --runtimes net10.0 net11.0\r\n\r\nusing BenchmarkDotNet.Running;\r\n\r\nusing System.Linq;\r\nusing BenchmarkDotNet.Attributes;\r\n\r\nBenchmarkSwitcher.FromAssembly(typeof(Benchmarks).Assembly).Run(args);\r\n\r\n[HideColumns(\"Job\", \"Error\", \"StdDev\", \"RatioSD\", \"Median\")]\r\npublic class Benchmarks\r\n{\r\n    private const int N = 32;\r\n    private int[] _intData = [];\r\n\r\n    [GlobalSetup]\r\n    public void Setup()\r\n    {\r\n        Random rng = new(42);\r\n        _intData = Enumerable.Range(0, N).Select(_ =&gt; rng.Next(-1_000, 1_000)).ToArray();\r\n    }\r\n\r\n    [Benchmark]\r\n    public int SumInt() =&gt; _intData.Sum();\r\n}<\/code><\/pre>\n<table>\n<thead>\n<tr>\n<th>Method<\/th>\n<th>Runtime<\/th>\n<th>Mean<\/th>\n<th>Ratio<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td>SumInt<\/td>\n<td>.NET 10.0<\/td>\n<td>5.609 ns<\/td>\n<td>1.00<\/td>\n<\/tr>\n<tr>\n<td>SumInt<\/td>\n<td>.NET 11.0<\/td>\n<td>4.731 ns<\/td>\n<td>0.84<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<p><code>Enumerable<\/code>&#8216;s <code>Min<\/code> and <code>Max<\/code> already examined many values at once with SIMD, but they still finished <code>byte<\/code>, <code>sbyte<\/code>, <code>short<\/code>, and <code>ushort<\/code> inputs by copying the final vector to the stack and checking its values one by one. <a href=\"https:\/\/github.com\/dotnet\/runtime\/pull\/127995\">dotnet\/runtime#127995<\/a> keeps that final step in vector instructions, using shuffles to combine the lanes. Smaller element types pack more values into each vector, so they benefit most from no longer finishing the search one value at a time.<\/p>\n<pre><code class=\"language-csharp\">\/\/ dotnet run -c Release -f net10.0 --filter \"*\" --runtimes net10.0 net11.0\r\n\r\nusing BenchmarkDotNet.Attributes;\r\nusing BenchmarkDotNet.Running;\r\n\r\nBenchmarkSwitcher.FromAssembly(typeof(Benchmarks).Assembly).Run(args);\r\n\r\n[HideColumns(\"Job\", \"Error\", \"StdDev\", \"Median\", \"RatioSD\")]\r\npublic class Benchmarks\r\n{\r\n    private byte[] _bytes = [];\r\n\r\n    [Params(16, 64)]\r\n    public int Length { get; set; }\r\n\r\n    [GlobalSetup]\r\n    public void Setup() =&gt; _bytes = Enumerable.Range(0, Length).Select(i =&gt; (byte)i).ToArray();\r\n\r\n    [Benchmark]\r\n    public byte MaxByte() =&gt; _bytes.Max();\r\n}<\/code><\/pre>\n<table>\n<thead>\n<tr>\n<th>Method<\/th>\n<th>Runtime<\/th>\n<th>Length<\/th>\n<th>Mean<\/th>\n<th>Ratio<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td>MaxByte<\/td>\n<td>.NET 10.0<\/td>\n<td>16<\/td>\n<td>7.546 ns<\/td>\n<td>1.00<\/td>\n<\/tr>\n<tr>\n<td>MaxByte<\/td>\n<td>.NET 11.0<\/td>\n<td>16<\/td>\n<td>2.176 ns<\/td>\n<td>0.29<\/td>\n<\/tr>\n<tr>\n<td>MaxByte<\/td>\n<td>.NET 10.0<\/td>\n<td>64<\/td>\n<td>7.827 ns<\/td>\n<td>1.00<\/td>\n<\/tr>\n<tr>\n<td>MaxByte<\/td>\n<td>.NET 11.0<\/td>\n<td>64<\/td>\n<td>2.059 ns<\/td>\n<td>0.26<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<p>Since its inception, LINQ has had <code>Join<\/code> and <code>GroupJoin<\/code>, and .NET 10\nintroduced the long-requested <code>LeftJoin<\/code> and <code>RightJoin<\/code>. In .NET 11, <a href=\"https:\/\/github.com\/dotnet\/runtime\/pull\/127236\">dotnet\/runtime#127236<\/a> adds <code>FullJoin<\/code>. The operators differ\nin which unmatched elements they retain and how they represent the matches:<\/p>\n<ul>\n<li><code>Join<\/code> emits only pairs whose keys match.<\/li>\n<li><code>GroupJoin<\/code> emits every left element together with a sequence containing its\nmatching right elements; that sequence is empty when there are no matches.<\/li>\n<li><code>LeftJoin<\/code> emits the matching pairs and also unmatched left elements, paired\nwith a default value for the right.<\/li>\n<li><code>RightJoin<\/code> does the inverse, emitting the matching pairs and also unmatched right elements, paired with a default value for the left.<\/li>\n<li><code>FullJoin<\/code> emits the matching pairs and the unmatched elements from both\ninputs, using a default value for whichever side is missing.<\/li>\n<\/ul>\n<p>Before .NET 11, applications typically approximated it by combining\n<code>GroupJoin<\/code>, <code>SelectMany<\/code>, and <code>Concat<\/code>, then searching the first input again\nto find right-side elements without a match. The built-in operator avoids both\nthat composition of iterators and the repeated search.<\/p>\n<pre><code class=\"language-csharp\">\/\/ dotnet run -c Release -f net11.0 --filter \"*\"\r\n\r\nusing BenchmarkDotNet.Running;\r\n\r\nusing System.Collections.Generic;\r\nusing System.Linq;\r\nusing BenchmarkDotNet.Attributes;\r\n\r\nBenchmarkSwitcher.FromAssembly(typeof(Benchmarks).Assembly).Run(args);\r\n\r\n[MemoryDiagnoser(false), HideColumns(\"Job\", \"Error\", \"StdDev\", \"RatioSD\", \"Median\")]\r\npublic class Benchmarks\r\n{\r\n    private const int N = 10_000;\r\n\r\n    private List&lt;(int Id, string Name)&gt; _left = Enumerable.Range(0, N).Select(i =&gt; (i, $\"item{i}\")).ToList();\r\n    private List&lt;(int Id, decimal Amount)&gt; _right = Enumerable.Range(N \/ 4, N).Select(i =&gt; (i, (decimal)i * 1.5m)).ToList();\r\n\r\n    [Benchmark(Baseline = true)]\r\n    public int FullJoin_Manual() =&gt;\r\n        _left.GroupJoin(_right, l =&gt; l.Id, r =&gt; r.Id, (l, rs) =&gt; (l, rs))\r\n             .SelectMany(x =&gt; x.rs.DefaultIfEmpty(), (x, r) =&gt; (x.l, r))\r\n             .Concat(_right\r\n                 .Where(r =&gt; !_left.Any(l =&gt; l.Id == r.Id))\r\n                 .Select(r =&gt; (l: default((int Id, string Name)), r)))\r\n             .Count();\r\n\r\n    [Benchmark]\r\n    public int FullJoin_New() =&gt; _left.FullJoin(_right, l =&gt; l.Id, r =&gt; r.Id).Count();\r\n}<\/code><\/pre>\n<p>For 10,000 elements in each input:<\/p>\n<table>\n<thead>\n<tr>\n<th>Method<\/th>\n<th style=\"text-align: right;\">Mean<\/th>\n<th style=\"text-align: right;\">Ratio<\/th>\n<th style=\"text-align: right;\">Allocated<\/th>\n<th style=\"text-align: right;\">Alloc Ratio<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td>FullJoin_Manual<\/td>\n<td style=\"text-align: right;\">27.615 ms<\/td>\n<td style=\"text-align: right;\">1.00<\/td>\n<td style=\"text-align: right;\">3.23 MB<\/td>\n<td style=\"text-align: right;\">1.00<\/td>\n<\/tr>\n<tr>\n<td>FullJoin_New<\/td>\n<td style=\"text-align: right;\">1.702 ms<\/td>\n<td style=\"text-align: right;\">0.06<\/td>\n<td style=\"text-align: right;\">1.56 MB<\/td>\n<td style=\"text-align: right;\">0.48<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<p>The earlier <code>Skip<\/code> example showed how many LINQ optimizations come from one operator flowing information to subsequent operators that can then be used for additional optimization. This is typically done by adding that additional information to properties on the concrete internal <code>IEnumerable&lt;T&gt;<\/code> implementations used by <code>System.Linq<\/code>. Synchronous <code>Enumerable<\/code> has accumulated many such specialized iterators over many releases, focusing in particular on places where algorithmic complexity could be significantly reduced. <code>AsyncEnumerable<\/code>, introduced &#8220;in the box&#8221; in .NET 10, initially had much less of that machinery; for asynchronous sequences dominated by I\/O, it often wouldn&#8217;t matter.<\/p>\n<p>Concatenation is an important exception. Prior to .NET 11, every call to\n<code>AsyncEnumerable.Append<\/code> created a new iterator around the sequence produced\nby the previous call. Consider a chain with just three appended values:<\/p>\n<pre><code class=\"language-csharp\">var sequence = AsyncEnumerable.Empty&lt;int&gt;()\r\n    .Append(0)\r\n    .Append(1)\r\n    .Append(2);<\/code><\/pre>\n<p>To produce <code>0<\/code>, enumeration needs to pass through all three nested iterators.\nProducing <code>1<\/code> passes through two, and producing <code>2<\/code> passes through one. Thus,\nyielding three values involves roughly <code>3 + 2 + 1<\/code> iterator steps. With 1,000\nappends, that grows to <code>1,000 + 999 + ... + 1<\/code>, or approximately 500,000\nsteps, rather than approximately 1,000. In general, enumerating N values\nrequires O(N^2) work. Several years back <code>Enumerable<\/code> addressed this by special-casing the various concatenation enumerables to flow enough information through to make iterating the chain O(N) rather than O(N^2), and in .NET 11, <a href=\"https:\/\/github.com\/dotnet\/runtime\/pull\/122389\">dotnet\/runtime#122389<\/a> applies that to <code>AsyncEnumerable<\/code> as well. The operators accumulate the extra elements or sequences in one flat representation instead of adding another wrapper for each LINQ operator.<\/p>\n<pre><code class=\"language-csharp\">\/\/ dotnet run -c Release -f net10.0 --filter \"*\" --runtimes net10.0 net11.0\r\n\r\nusing BenchmarkDotNet.Running;\r\n\r\nusing System.Linq;\r\nusing System.Threading.Tasks;\r\nusing BenchmarkDotNet.Attributes;\r\n\r\nBenchmarkSwitcher.FromAssembly(typeof(Benchmarks).Assembly).Run(args);\r\n\r\n[MemoryDiagnoser(false), HideColumns(\"Job\", \"Error\", \"StdDev\", \"RatioSD\", \"Median\")]\r\npublic class Benchmarks\r\n{\r\n    [Benchmark]\r\n    public async Task&lt;int&gt; AppendChain()\r\n    {\r\n        var seq = AsyncEnumerable.Empty&lt;int&gt;();\r\n        for (int i = 0; i &lt; 1_000; i++) seq = seq.Append(i);\r\n        return await seq.SumAsync();\r\n    }\r\n}<\/code><\/pre>\n<p>For a chain of 1,000 appended elements:<\/p>\n<table>\n<thead>\n<tr>\n<th>Method<\/th>\n<th>Runtime<\/th>\n<th>Mean<\/th>\n<th>Ratio<\/th>\n<th>Allocated<\/th>\n<th>Alloc Ratio<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td>AppendChain<\/td>\n<td>.NET 10.0<\/td>\n<td>8.253 ms<\/td>\n<td>1.00<\/td>\n<td>187.57 KB<\/td>\n<td>1.00<\/td>\n<\/tr>\n<tr>\n<td>AppendChain<\/td>\n<td>.NET 11.0<\/td>\n<td>28.49 \u03bcs<\/td>\n<td>0.00345<\/td>\n<td>136.77 KB<\/td>\n<td>0.73<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<h2>I\/O<\/h2>\n<p>I\/O performance is often equated with the speed of the underlying device, but\nthe transfer itself is only one part of an operation. A cached file read may\ncomplete in microseconds, many redirected pipes may be active concurrently,\nand compression may operate entirely on data already in memory. In cases like\nthese, the managed overhead around the operation can be as important as the\ntime spent moving the data.<\/p>\n<p>That overhead includes setting up the appropriate synchronous or asynchronous\nOS mechanism, keeping state alive until an operation completes,\nallocating and copying temporary buffers, and adapting between the caller&#8217;s\ndata and stream-based APIs. .NET 11 removes work from each of these layers.<\/p>\n<p>On Windows, &#8220;overlapped I\/O&#8221; is the asynchronous model in which an operation begins now and the operating system posts its completion later. Any time .NET performs I\/O as part of an asynchronous operation on Windows, it strives to use a corresponding overlapped I\/O API rather than using a synchronous API asynchronously (i.e. queuing a work item that blocks a thread pool thread doing the I\/O). However, there have been some stragglers. Redirected child-process output previously used synchronous pipe handles, so <code>ReadToEndAsync<\/code> on the stream from a <code>Process<\/code>&#8216;s stdout or stderr <code>Stream<\/code> still needed a thread-pool thread blocked in a native read for each stdout or stderr pipe. In .NET 11, <a href=\"https:\/\/github.com\/dotnet\/runtime\/pull\/125643\">dotnet\/runtime#125643<\/a> instead opens the parent&#8217;s stdout and stderr ends for overlapped reads, while leaving the child ends synchronous (as console applications expect).<\/p>\n<pre><code class=\"language-csharp\">\/\/ dotnet run -c Release -f net10.0 --filter \"*\" --runtimes net10.0 net11.0\r\n\r\nusing System.Diagnostics;\r\nusing BenchmarkDotNet.Attributes;\r\nusing BenchmarkDotNet.Jobs;\r\nusing BenchmarkDotNet.Running;\r\n\r\nif (args is [\"--emit\"])\r\n{\r\n    Console.Write(new string('x', 8 * 1024 * 1024));\r\n    return;\r\n}\r\n\r\nBenchmarkSwitcher.FromAssembly(typeof(Benchmarks).Assembly).Run(args);\r\n\r\n[SimpleJob(launchCount: 1, warmupCount: 3, iterationCount: 10)]\r\n[HideColumns(\"Job\", \"Error\", \"StdDev\", \"Median\", \"RatioSD\")]\r\npublic class Benchmarks\r\n{\r\n    [Benchmark]\r\n    public Task ReadOutputConcurrently() =&gt;\r\n        Task.WhenAll(Enumerable.Range(0, 16).Select(_ =&gt; RunProcess()));\r\n\r\n    private static async Task RunProcess()\r\n    {\r\n        var psi = new ProcessStartInfo(\"dotnet\")\r\n        {\r\n            RedirectStandardOutput = true,\r\n            UseShellExecute = false,\r\n        };\r\n        psi.ArgumentList.Add(typeof(Benchmarks).Assembly.Location);\r\n        psi.ArgumentList.Add(\"--emit\");\r\n\r\n        using Process process = Process.Start(psi)!;\r\n        _ = await process.StandardOutput.ReadToEndAsync();\r\n        await process.WaitForExitAsync();\r\n    }\r\n}<\/code><\/pre>\n<table>\n<thead>\n<tr>\n<th>Method<\/th>\n<th>Runtime<\/th>\n<th>Mean<\/th>\n<th>Ratio<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td>ReadOutputConcurrently<\/td>\n<td>.NET 10.0<\/td>\n<td>587.8 ms<\/td>\n<td>1.00<\/td>\n<\/tr>\n<tr>\n<td>ReadOutputConcurrently<\/td>\n<td>.NET 11.0<\/td>\n<td>541.8 ms<\/td>\n<td>0.92<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<p>We often refer to the mechanism being fixed as &#8220;async over sync.&#8221; The opposite\ncase, &#8220;sync over async,&#8221; can be even worse, as it means blocking one thread\nwhile waiting for another to do some work; that provides one of the necessary\ningredients for cycles and deadlocks, and is a leading cause of scalability\nbottlenecks in services, so we try to stamp out &#8220;sync over async&#8221; whenever\npossible. In cases where we can&#8217;t avoid it, though, we can at least make it\nbetter.<\/p>\n<p><code>RandomAccess.Read<\/code> provides one such opportunity when it&#8217;s used with a\nWindows file handle opened for asynchronous I\/O. Windows requires specifying at the time of file opening whether I\/O will be overlapped or not, and if it is, Windows still requires a read to use its <code>OVERLAPPED<\/code> mechanism, even in a synchronous <code>Read<\/code> case where the API&#8217;s caller is going to block until that read completes. While we can&#8217;t avoid that overlapped I\/O, we can still make the operation cheaper. Previously, .NET both gave the operation an event for the calling thread to wait on and registered an I\/O-completion callback. Instead, <a href=\"https:\/\/github.com\/dotnet\/runtime\/pull\/126845\">dotnet\/runtime#126845<\/a> uses a\ndocumented Windows convention: setting the low bit of <code>OVERLAPPED.hEvent<\/code>\ninstructs Windows to signal the event when the operation completes but not to\nalso queue a completion packet to the I\/O completion port. The calling thread\ncan wait on an event cached by the file handle, retrieve the result, and\nperform the cleanup itself. This removes the callback, its coordination, and\nthe per-operation allocation while still performing the same synchronous\nwait.<\/p>\n<pre><code class=\"language-csharp\">\/\/ Windows:\r\n\/\/ dotnet run -c Release -f net10.0 --filter \"*RandomAccessBenchmarks*\" --runtimes net10.0 net11.0\r\n\r\nusing BenchmarkDotNet.Attributes;\r\nusing BenchmarkDotNet.Running;\r\nusing Microsoft.Win32.SafeHandles;\r\n\r\nBenchmarkSwitcher.FromAssembly(typeof(RandomAccessBenchmarks).Assembly).Run(args);\r\n\r\n[MemoryDiagnoser(false), HideColumns(\"Job\", \"Error\", \"StdDev\", \"Median\", \"RatioSD\")]\r\npublic class RandomAccessBenchmarks\r\n{\r\n    private string _path = \"\";\r\n    private SafeFileHandle _handle = null!;\r\n    private readonly byte[] _buffer = new byte[4_096];\r\n\r\n    [GlobalSetup]\r\n    public void Setup()\r\n    {\r\n        _path = Path.Combine(Path.GetTempPath(), $\"net11-random-access-{Guid.NewGuid():N}.tmp\");\r\n        File.WriteAllBytes(_path, new byte[1024 * 1024]);\r\n        _handle = File.OpenHandle(_path, FileMode.Open, FileAccess.Read, FileShare.Read, FileOptions.Asynchronous | FileOptions.RandomAccess);\r\n    }\r\n\r\n    [GlobalCleanup]\r\n    public void Cleanup()\r\n    {\r\n        _handle.Dispose();\r\n        File.Delete(_path);\r\n    }\r\n\r\n    [Benchmark]\r\n    public int Read4K() =&gt; RandomAccess.Read(_handle, _buffer, fileOffset: 0);\r\n}<\/code><\/pre>\n<table>\n<thead>\n<tr>\n<th>Method<\/th>\n<th>Runtime<\/th>\n<th>Mean<\/th>\n<th>Ratio<\/th>\n<th>Allocated<\/th>\n<th>Alloc Ratio<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td>Read4K<\/td>\n<td>.NET 10.0<\/td>\n<td>4.564 \u03bcs<\/td>\n<td>1.00<\/td>\n<td>176 B<\/td>\n<td>1.00<\/td>\n<\/tr>\n<tr>\n<td>Read4K<\/td>\n<td>.NET 11.0<\/td>\n<td>2.747 \u03bcs<\/td>\n<td>0.60<\/td>\n<td>&#8211;<\/td>\n<td>0<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<p>There are smaller allocation wins at higher layers as well. For example, <a href=\"https:\/\/github.com\/dotnet\/runtime\/pull\/121508\">dotnet\/runtime#121508<\/a> makes assigning <code>TextWriter.NewLine<\/code> to its existing value a no-op and shares arrays for the standard <code>\"\\n\"<\/code> and <code>\"\\r\\n\"<\/code> values, avoiding a fresh <code>char[]<\/code> conversion.<\/p>\n<pre><code class=\"language-csharp\">\/\/ dotnet run -c Release -f net10.0 --filter \"*\" --runtimes net10.0 net11.0\r\n\r\nusing BenchmarkDotNet.Attributes;\r\nusing BenchmarkDotNet.Running;\r\n\r\nBenchmarkSwitcher.FromAssembly(typeof(Benchmarks).Assembly).Run(args);\r\n\r\n[MemoryDiagnoser(false), HideColumns(\"Job\", \"Error\", \"StdDev\", \"Median\", \"RatioSD\")]\r\npublic class Benchmarks\r\n{\r\n    private readonly StringWriter _writer = new();\r\n\r\n    [Benchmark]\r\n    public void SetSameValue() =&gt; _writer.NewLine = Environment.NewLine;\r\n}<\/code><\/pre>\n<table>\n<thead>\n<tr>\n<th>Method<\/th>\n<th>Runtime<\/th>\n<th>Mean<\/th>\n<th>Ratio<\/th>\n<th>Allocated<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td>SetSameValue<\/td>\n<td>.NET 10.0<\/td>\n<td>8.281 ns<\/td>\n<td>1.00<\/td>\n<td>32 B<\/td>\n<\/tr>\n<tr>\n<td>SetSameValue<\/td>\n<td>.NET 11.0<\/td>\n<td>2.168 ns<\/td>\n<td>0.26<\/td>\n<td>&#8211;<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<p>Archive and file APIs remove similar temporary allocations. A GNU tar header\nhas fixed-size fields for an entry&#8217;s name and link target. When either doesn&#8217;t\nfit, <code>TarWriter<\/code> emits an additional metadata record containing the long\nvalue. In .NET 10, <code>TarWriter<\/code> first encoded that value into a newly allocated\nbyte array, then wrote the bytes and a null terminator into a new\n<code>MemoryStream<\/code>. Because the stream didn&#8217;t know the final size, it allocated\nand grew its own backing array as the data was written. With\n<a href=\"https:\/\/github.com\/dotnet\/runtime\/pull\/123835\">dotnet\/runtime#123835<\/a>, .NET 11\ninstead computes the exact UTF-8 size including the terminator, allocates one\narray of that size, encodes directly into it, and constructs the\n<code>MemoryStream<\/code> over that array. This removes both the temporary encoded array\nand the stream&#8217;s growth and copying.<\/p>\n<pre><code class=\"language-csharp\">\/\/ dotnet run -c Release -f net10.0 --filter \"*\" --runtimes net10.0 net11.0\r\n\r\nusing System.Formats.Tar;\r\nusing BenchmarkDotNet.Attributes;\r\nusing BenchmarkDotNet.Running;\r\n\r\nBenchmarkSwitcher.FromAssembly(typeof(Benchmarks).Assembly).Run(args);\r\n\r\n[MemoryDiagnoser(false)]\r\n[HideColumns(\"Job\", \"Error\", \"StdDev\", \"Median\", \"RatioSD\")]\r\npublic class Benchmarks\r\n{\r\n    private readonly MemoryStream _destination = new();\r\n    private readonly GnuTarEntry _entry = new(TarEntryType.RegularFile, new string('a', 256));\r\n\r\n    [Benchmark]\r\n    public long WriteLongName()\r\n    {\r\n        _destination.SetLength(0);\r\n        using TarWriter writer = new(_destination, TarEntryFormat.Gnu, leaveOpen: true);\r\n        writer.WriteEntry(_entry);\r\n        return _destination.Length;\r\n    }\r\n}<\/code><\/pre>\n<table>\n<thead>\n<tr>\n<th>Method<\/th>\n<th>Runtime<\/th>\n<th style=\"text-align: right;\">Mean<\/th>\n<th style=\"text-align: right;\">Ratio<\/th>\n<th style=\"text-align: right;\">Allocated<\/th>\n<th style=\"text-align: right;\">Alloc Ratio<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td>WriteLongName<\/td>\n<td>.NET 10.0<\/td>\n<td style=\"text-align: right;\">731.1 ns<\/td>\n<td style=\"text-align: right;\">1.00<\/td>\n<td style=\"text-align: right;\">1.36 KB<\/td>\n<td style=\"text-align: right;\">1.00<\/td>\n<\/tr>\n<tr>\n<td>WriteLongName<\/td>\n<td>.NET 11.0<\/td>\n<td style=\"text-align: right;\">616.7 ns<\/td>\n<td style=\"text-align: right;\">0.84<\/td>\n<td style=\"text-align: right;\">608 B<\/td>\n<td style=\"text-align: right;\">0.44<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<p><code>ZipArchive<\/code> similarly eliminates temporary buffers. ZIP archives end with a\ncentral directory describing their entries. Reading that directory allocated\na new 4 KB buffer for every <code>ZipArchive<\/code>, along with additional arrays in a\nfew paths that needed to combine or slice data. With\n<a href=\"https:\/\/github.com\/dotnet\/runtime\/pull\/123836\">dotnet\/runtime#123836<\/a>, .NET 11\nnow rents the central-directory buffer from <code>ArrayPool&lt;byte&gt;<\/code> and uses spans\nand memory in place of the additional arrays and several open-coded loops.<\/p>\n<pre><code class=\"language-csharp\">\/\/ dotnet run -c Release -f net10.0 --filter \"*\" --runtimes net10.0 net11.0\r\n\r\nusing System.IO.Compression;\r\nusing BenchmarkDotNet.Attributes;\r\nusing BenchmarkDotNet.Running;\r\n\r\nBenchmarkSwitcher.FromAssembly(typeof(Benchmarks).Assembly).Run(args);\r\n\r\n[MemoryDiagnoser(false)]\r\n[HideColumns(\"Job\", \"Error\", \"StdDev\", \"Median\", \"RatioSD\")]\r\npublic class Benchmarks\r\n{\r\n    private byte[] _archive = [];\r\n\r\n    [GlobalSetup]\r\n    public void Setup()\r\n    {\r\n        using MemoryStream destination = new();\r\n        using (ZipArchive archive = new(destination, ZipArchiveMode.Create, leaveOpen: true))\r\n        {\r\n            ZipArchiveEntry entry = archive.CreateEntry(\"entry.txt\");\r\n            using Stream stream = entry.Open();\r\n            stream.WriteByte(42);\r\n        }\r\n\r\n        _archive = destination.ToArray();\r\n    }\r\n\r\n    [Benchmark]\r\n    public int ReadCentralDirectory()\r\n    {\r\n        using MemoryStream source = new(_archive, writable: false);\r\n        using ZipArchive archive = new(source, ZipArchiveMode.Read);\r\n        return archive.Entries.Count;\r\n    }\r\n}<\/code><\/pre>\n<table>\n<thead>\n<tr>\n<th>Method<\/th>\n<th>Runtime<\/th>\n<th>Mean<\/th>\n<th>Ratio<\/th>\n<th>Allocated<\/th>\n<th>Alloc Ratio<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td>ReadCentralDirectory<\/td>\n<td>.NET 10.0<\/td>\n<td>553.5 ns<\/td>\n<td>1.00<\/td>\n<td>5.05 KB<\/td>\n<td>1.00<\/td>\n<\/tr>\n<tr>\n<td>ReadCentralDirectory<\/td>\n<td>.NET 11.0<\/td>\n<td>258.1 ns<\/td>\n<td>0.47<\/td>\n<td>1.13 KB<\/td>\n<td>0.22<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<p>As a final example, <code>FileInfo.MoveTo<\/code> performs a source-directory existence check before moving the file. It had been constructing a <code>DirectoryInfo<\/code> solely to read its <code>Exists<\/code> property, which is wasteful when <code>Directory.Exists<\/code> exists and can do it without the allocation. <a href=\"https:\/\/github.com\/dotnet\/runtime\/pull\/123893\">dotnet\/runtime#123893<\/a> in .NET 11 switches to use that.<\/p>\n<pre><code class=\"language-csharp\">\/\/ dotnet run -c Release -f net10.0 --filter \"*\" --runtimes net10.0 net11.0\r\n\r\nusing BenchmarkDotNet.Attributes;\r\nusing BenchmarkDotNet.Running;\r\n\r\nBenchmarkSwitcher.FromAssembly(typeof(Benchmarks).Assembly).Run(args);\r\n\r\n[MemoryDiagnoser(false)]\r\n[HideColumns(\"Job\", \"Error\", \"StdDev\", \"Median\", \"RatioSD\")]\r\npublic class Benchmarks\r\n{\r\n    private string _directory = \"\";\r\n    private string _firstPath = \"\";\r\n    private string _secondPath = \"\";\r\n    private FileInfo _file = null!;\r\n    private bool _atFirstPath;\r\n\r\n    [GlobalSetup]\r\n    public void Setup()\r\n    {\r\n        _directory = Path.Combine(Path.GetTempPath(), $\"net11-file-move-{Guid.NewGuid():N}\");\r\n        Directory.CreateDirectory(_directory);\r\n        _firstPath = Path.Combine(_directory, \"first.tmp\");\r\n        _secondPath = Path.Combine(_directory, \"second.tmp\");\r\n        File.WriteAllBytes(_firstPath, [42]);\r\n        _file = new(_firstPath);\r\n        _atFirstPath = true;\r\n    }\r\n\r\n    [GlobalCleanup]\r\n    public void Cleanup() =&gt; Directory.Delete(_directory, recursive: true);\r\n\r\n    [Benchmark]\r\n    public string MoveTo()\r\n    {\r\n        _file.MoveTo(_atFirstPath ? _secondPath : _firstPath, overwrite: true);\r\n        _atFirstPath = !_atFirstPath;\r\n        return _file.FullName;\r\n    }\r\n}<\/code><\/pre>\n<table>\n<thead>\n<tr>\n<th>Method<\/th>\n<th>Runtime<\/th>\n<th>Allocated<\/th>\n<th>Alloc Ratio<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td>MoveTo<\/td>\n<td>.NET 10.0<\/td>\n<td>332 B<\/td>\n<td>1.00<\/td>\n<\/tr>\n<tr>\n<td>MoveTo<\/td>\n<td>.NET 11.0<\/td>\n<td>236 B<\/td>\n<td>0.71<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<p>.NET provides great high-level abstractions for working with all manner of\ndata and I\/O. One of the most prominent is <code>Stream<\/code>, which provides a simple,\nflexible mechanism for reading and writing many different data sources and\nformats: <code>MemoryStream<\/code>, <code>FileStream<\/code>, <code>CryptoStream<\/code>, <code>ZLibStream<\/code>,\n<code>SslStream<\/code>, and on and on. For folks that really care about maximizing\nperformance, however, sometimes you want to go a bit lower-level and deal\ndirectly with the underlying primitives. For example, the various compression\nstreams, <code>ZLibStream<\/code>, <code>DeflateStream<\/code>, <code>GZipStream<\/code>, and <code>BrotliStream<\/code>, all\nmaintain buffers that hold input and output data waiting to be read or written.\nBut what if the caller already owns its input and output buffers, or wants to\nuse a pool for them? In .NET 11,\n<a href=\"https:\/\/github.com\/dotnet\/runtime\/pull\/123145\">dotnet\/runtime#123145<\/a> exposes\nthe underlying <code>DeflateEncoder<\/code>\/<code>DeflateDecoder<\/code>,\n<code>ZLibEncoder<\/code>\/<code>ZLibDecoder<\/code>, and <code>GZipEncoder<\/code>\/<code>GZipDecoder<\/code> types, following\nthe existing <code>BrotliEncoder<\/code> and <code>BrotliDecoder<\/code> pattern. The stream types are wrappers around these encoders and decoders, and in\n.NET 11 you can now use them directly. They support chunked <code>Compress<\/code>,\n<code>Decompress<\/code>, and <code>Flush<\/code> operations as well as one-shot <code>TryCompress<\/code> and\n<code>TryDecompress<\/code>, enabling callers to supply and reuse their own buffers rather\nthan going through adapter streams and their buffers.<\/p>\n<h2>Networking<\/h2>\n<p>Networking is the bread-and-butter of many applications and sits directly on the hot path of scalable services. Improvements throughout the stack add up quickly.<\/p>\n<p>At the bottom of the stack, connections and sockets establish and carry the\nbyte stream. A socket doesn&#8217;t actually connect to a host name; it connects to\nan IP address and port. Resolving a host name may produce multiple candidate\naddresses, including one or more IPv4 addresses from DNS A records and one or\nmore IPv6 addresses from AAAA records. When <code>SocketAsyncEventArgs.RemoteEndPoint<\/code> is a <code>DnsEndPoint<\/code>, the existing <code>Socket.ConnectAsync<\/code> implementation performs that resolution and tries the resulting addresses in sequence. It starts a connection to the first address, and only if that attempt fails does it move on to the next. This works well when the first address is reachable. A failed TCP connection isn&#8217;t always reported quickly, however. If packets sent over that route are simply dropped, the attempt may remain pending until a timeout even though another address for the same host could have connected immediately.<\/p>\n<p>This problem is especially visible on machines with both IPv4 and IPv6.\nClients generally want to prefer IPv6 when it works, but a broken or\nmisconfigured IPv6 path can make an application wait through a long\ntimeout before trying IPv4. The technique commonly known as\n<a href=\"https:\/\/www.rfc-editor.org\/rfc\/rfc8305.html\">Happy Eyeballs<\/a> addresses this by\noverlapping connection attempts. Rather than putting all the latency of one\ncandidate in front of the next, it starts another attempt after a short delay\nand uses the first connection that succeeds. The remaining attempts are then\ncanceled or discarded. That consumes some additional resources, but it can\ngreatly reduce the long tail of connection establishment.<\/p>\n<p>In .NET 11, <a href=\"https:\/\/github.com\/dotnet\/runtime\/pull\/106374\">dotnet\/runtime#106374<\/a> adds an opt-in, Happy-Eyeballs-like strategy to the static <code>Socket.ConnectAsync<\/code>\noverload that accepts a <code>SocketAsyncEventArgs<\/code>. The new overload accepts\na <code>ConnectAlgorithm<\/code>, where <code>ConnectAlgorithm.Default<\/code> preserves the existing\nsequential behavior, while <code>ConnectAlgorithm.Parallel<\/code> requests the new\nstrategy. When parallel connection is requested for an address-family-unspecified\n<code>DnsEndPoint<\/code> on a machine that supports both IPv4 and IPv6, .NET starts\nseparate IPv4 and IPv6 DNS queries and runs a connection loop for each family\nconcurrently. Addresses within each family are still tried sequentially, but\nthe two families no longer wait on each other. The first successful connection\nbecomes the <code>ConnectSocket<\/code>, and a connection subsequently established by the\nother family is disposed. If one family fails, the other is allowed to\ncontinue; the operation reports failure only after neither can connect.\nParallel mode can briefly establish two connections, while the default remains\ncheaper when the first candidate connects promptly.<\/p>\n<p>Given the nature of the change, it&#8217;s a little hard to create a real benchmark\nfor this, but we can get creative. Here I&#8217;ve created IPv4\nand IPv6 listeners on the same port, but arranged the benchmark to only accept\nfrom the IPv4 listener. On my machine, <code>localhost<\/code> resolves to <code>::1<\/code> before\n<code>127.0.0.1<\/code>, so the client sockets by default would first try the IPv6 address\nand only when it fails try the IPv4 one. The benchmark setup fills the IPv6\nlistener&#8217;s accept backlog, such that additional client connect requests will\nstall.<\/p>\n<pre><code class=\"language-csharp\">\/\/ Windows\r\n\/\/ dotnet run -c Release -f net11.0 --filter \"*\"\r\n\r\nusing System.Net;\r\nusing System.Net.Sockets;\r\nusing BenchmarkDotNet.Attributes;\r\nusing BenchmarkDotNet.Engines;\r\nusing BenchmarkDotNet.Running;\r\n\r\nBenchmarkRunner.Run&lt;Benchmarks&gt;();\r\n\r\n[SimpleJob(RunStrategy.Throughput, launchCount: 1, warmupCount: 2, iterationCount: 8, invocationCount: 1)]\r\n[HideColumns(\"Job\", \"Error\", \"StdDev\", \"Median\", \"RatioSD\")]\r\npublic class Benchmarks\r\n{\r\n    private Socket _ipv6Listener = null!;\r\n    private Socket _ipv4Listener = null!;\r\n    private List&lt;Socket&gt; _backlogClients = [];\r\n    private CancellationTokenSource _cancellation = null!;\r\n    private Task _ipv4AcceptLoop = null!;\r\n    private Task? _releaseOne;\r\n    private int _port;\r\n\r\n    [IterationSetup(Target = nameof(Default))]\r\n    public void SetupDefault() =&gt; Setup(releaseIPv6: true);\r\n\r\n    [IterationSetup(Target = nameof(Parallel))]\r\n    public void SetupParallel() =&gt; Setup(releaseIPv6: false);\r\n\r\n    [IterationCleanup]\r\n    public void Cleanup()\r\n    {\r\n        _releaseOne?.GetAwaiter().GetResult();\r\n        _cancellation.Cancel();\r\n        _ipv4Listener.Dispose();\r\n        _ipv6Listener.Dispose();\r\n\r\n        try\r\n        {\r\n            _ipv4AcceptLoop.GetAwaiter().GetResult();\r\n        }\r\n        catch { }\r\n\r\n        foreach (Socket socket in _backlogClients)\r\n        {\r\n            socket.Dispose();\r\n        }\r\n\r\n        _cancellation.Dispose();\r\n    }\r\n\r\n    [Benchmark(Baseline = true)]\r\n    public async Task Default()\r\n    {\r\n        using Socket socket = await ConnectAsync(ConnectAlgorithm.Default);\r\n    }\r\n\r\n    [Benchmark]\r\n    public async Task Parallel()\r\n    {\r\n        using Socket socket = await ConnectAsync(ConnectAlgorithm.Parallel);\r\n    }\r\n\r\n    private void Setup(bool releaseIPv6)\r\n    {\r\n        if (Dns.GetHostAddresses(\"localhost\")[0].AddressFamily != AddressFamily.InterNetworkV6)\r\n        {\r\n            throw new InvalidOperationException(\"This benchmark requires localhost to prefer IPv6.\");\r\n        }\r\n\r\n        _ipv6Listener = new(AddressFamily.InterNetworkV6, SocketType.Stream, ProtocolType.Tcp)\r\n        {\r\n            DualMode = false,\r\n        };\r\n        _ipv6Listener.Bind(new IPEndPoint(IPAddress.IPv6Loopback, 0));\r\n        _port = ((IPEndPoint)_ipv6Listener.LocalEndPoint!).Port;\r\n        _ipv6Listener.Listen(1);\r\n\r\n        _ipv4Listener = new(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);\r\n        _ipv4Listener.Bind(new IPEndPoint(IPAddress.Loopback, _port));\r\n        _ipv4Listener.Listen(128);\r\n        _cancellation = new();\r\n        _ipv4AcceptLoop = AcceptLoopAsync(_ipv4Listener, _cancellation.Token);\r\n\r\n        _backlogClients = [];\r\n        bool stalled = false;\r\n        for (int i = 0; i &lt; 128; i++)\r\n        {\r\n            Socket socket = new(AddressFamily.InterNetworkV6, SocketType.Stream, ProtocolType.Tcp);\r\n            Task connect = socket.ConnectAsync(new IPEndPoint(IPAddress.IPv6Loopback, _port));\r\n\r\n            if (!connect.Wait(TimeSpan.FromMilliseconds(100)))\r\n            {\r\n                socket.Dispose();\r\n                stalled = true;\r\n                break;\r\n            }\r\n\r\n            connect.GetAwaiter().GetResult();\r\n            _backlogClients.Add(socket);\r\n        }\r\n\r\n        if (!stalled)\r\n        {\r\n            throw new InvalidOperationException(\"Unable to saturate the IPv6 accept backlog.\");\r\n        }\r\n\r\n        _releaseOne = releaseIPv6 ?\r\n            Task.Run(async () =&gt;\r\n            {\r\n                await Task.Delay(100);\r\n                using Socket socket = await _ipv6Listener.AcceptAsync();\r\n            }) :\r\n            null;\r\n    }\r\n\r\n    private Task&lt;Socket&gt; ConnectAsync(ConnectAlgorithm algorithm)\r\n    {\r\n        TaskCompletionSource&lt;Socket&gt; completion = new(TaskCreationOptions.RunContinuationsAsynchronously);\r\n        var args = new SocketAsyncEventArgs\r\n        {\r\n            RemoteEndPoint = new DnsEndPoint(\"localhost\", _port),\r\n        };\r\n\r\n        args.Completed += Complete;\r\n        if (!Socket.ConnectAsync(SocketType.Stream, ProtocolType.Tcp, args, algorithm))\r\n        {\r\n            Complete(null, args);\r\n        }\r\n\r\n        return completion.Task;\r\n\r\n        void Complete(object? sender, SocketAsyncEventArgs e)\r\n        {\r\n            e.Completed -= Complete;\r\n            if (e.SocketError == SocketError.Success)\r\n            {\r\n                completion.SetResult(e.ConnectSocket!);\r\n            }\r\n            else\r\n            {\r\n                completion.SetException(new SocketException((int)e.SocketError));\r\n            }\r\n\r\n            e.Dispose();\r\n        }\r\n    }\r\n\r\n    private static async Task AcceptLoopAsync(Socket listener, CancellationToken cancellationToken)\r\n    {\r\n        while (true)\r\n        {\r\n            using Socket socket = await listener.AcceptAsync(cancellationToken);\r\n        }\r\n    }\r\n}<\/code><\/pre>\n<p>With this setup, the parallel algorithm isn&#8217;t held up by the stalled IPv6\nattempt. It connects to the IPv4 listener in just over a millisecond, whereas\nthe default algorithm spends approximately half a second waiting for the IPv6\nconnection to make progress:<\/p>\n<table>\n<thead>\n<tr>\n<th>Method<\/th>\n<th>Mean<\/th>\n<th>Ratio<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td>Default<\/td>\n<td>511.054 ms<\/td>\n<td>1.000<\/td>\n<\/tr>\n<tr>\n<td>Parallel<\/td>\n<td>1.060 ms<\/td>\n<td>0.002<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<p>A bit synthetic, but it conveys the idea.<\/p>\n<p>Another interesting improvement around sockets has to do with <code>Socket.Blocking<\/code>. &#8220;Berkeley sockets&#8221;, which is what all modern stacks implement, implement the notion of blocking \/ non-blocking modes. Typically by default, as is the case with .NET, sockets are in blocking mode. That means, for example, a <code>recv()<\/code> \/ <code>Socket.Receive<\/code> operation will synchronously block until data is available (or the socket closes). The other option is non-blocking; a socket in non-blocking mode will always return immediately from a <code>recv<\/code> operation, regardless of whether there&#8217;s data to read or not. If the operation would have blocked in blocking mode, in non-blocking mode it&#8217;ll instead return an error code, EAGAIN or EWOULDBLOCK, which the consuming application can then use to, for example, decide to try again later.<\/p>\n<p>Enter .NET asynchronous operations. On Windows, the Windows sockets APIs provided overlapped APIs that the .NET <code>Socket<\/code> APIs can and do employ. But on Unix, we have the standard <code>recv<\/code> and friends functions. We also have mechanisms like <code>epoll<\/code> (Linux) and <code>kqueue<\/code> (macOS) that let us efficiently and synchronously wait for large numbers of file descriptors to have some activity. As such, on Unix, .NET implements asynchronous socket operations by putting a <code>Socket<\/code> into a non-blocking state, trying the synchronous operation (e.g. <code>recv<\/code> for a <code>Socket.ReceiveAsync<\/code>), and then if the operation couldn&#8217;t complete yet and we get back an EAGAIN\/EWOULDBLOCK, data about the operation gets queued into <code>epoll<\/code>\/<code>kqueue<\/code>-based machinery that will signal when the operation should be retried. This is very similar conceptually to how overlapped I\/O works on Windows with I\/O completion ports.<\/p>\n<p>Now here&#8217;s the rub. To implement asynchronous operations on sockets, we need to flip the socket into non-blocking mode&#8230; what do we then do if, say, someone does <code>Socket.ReceiveAsync<\/code> but then follows that up with <code>Socket.Send<\/code>. The socket was flipped into non-blocking mode for the first operation&#8230; do we flip it back for the second? It turns out that&#8217;s really risky to do, with race conditions making it hard and expensive to get right due to multi-threaded use (expensive because we&#8217;d need extra synchronization). When first bringing up .NET on Linux, we made the decision that the flip would be a one-way trip: once non-blocking, always non-blocking. We flip the first time an asynchronous operation is performed, and we leave it there.<\/p>\n<p>What, then, do we do if someone does in fact issue a synchronous <code>Receive<\/code>\/<code>Send<\/code> after it&#8217;s already been flipped? We simulate the blocking ourselves with sync over async, basically doing the asynchronous operation and blocking on it to complete. Internally we&#8217;re able to do it cheaper than actually creating a task and blocking on it, but as a mechanism it&#8217;s basically the same.<\/p>\n<p>We were comfortable with this approach in the early days on the theory that if someone starts using asynchronous operations, they&#8217;re likely to continue to, and for the odd synchronous operation here and there after that, it&#8217;s not a big deal. That has largely proven out over the many years since&#8230; except for one case.<\/p>\n<p>Turns out in some systems it&#8217;s reasonably common for the initial connect to be asynchronous but then followed only by synchronous sends and receives. This ends up paying that overhead on all operations: you do <code>ConnectAsync<\/code>, we flip the socket to be non-blocking, and then every <code>Receive<\/code>\/<code>Send<\/code> after that ends up paying the emulation costs. But there&#8217;s good news. It turns out this is also a case where we can easily and safely flip back: we can flip back to blocking before handing back control from <code>ConnectAsync<\/code>. For the static <code>ConnectAsync<\/code> overloads, the caller won&#8217;t even have a reference to the connected <code>Socket<\/code> until <code>ConnectAsync<\/code> gives it to them, and for the instance overloads, it&#8217;s defined to be erroneous to use such send\/receive operations on the <code>Socket<\/code> concurrent with <code>ConnectAsync<\/code>. As such, in all cases, we can just flip it back to blocking before completing the task representing the operation. That&#8217;s exactly what <a href=\"https:\/\/github.com\/dotnet\/runtime\/pull\/124200\">dotnet\/runtime#124200<\/a> does now in .NET 11.<\/p>\n<pre><code class=\"language-csharp\">\/\/ Linux:\r\n\/\/ dotnet run -c Release -f net10.0 --filter \"*\" --runtimes net10.0 net11.0\r\n\r\nusing System.Net;\r\nusing System.Net.Sockets;\r\nusing BenchmarkDotNet.Attributes;\r\nusing BenchmarkDotNet.Running;\r\n\r\nBenchmarkSwitcher.FromAssembly(typeof(Benchmarks).Assembly).Run(args);\r\n\r\n[HideColumns(\"Job\", \"Error\", \"StdDev\", \"Median\", \"RatioSD\")]\r\npublic class Benchmarks\r\n{\r\n    private const int OperationsPerInvoke = 1_000;\r\n    private readonly byte[] _buffer = new byte[1];\r\n    private Socket _listener = null!;\r\n    private Socket _client = null!;\r\n    private Socket _server = null!;\r\n    private Task _echoLoop = null!;\r\n\r\n    [GlobalSetup]\r\n    public async Task Setup()\r\n    {\r\n        _listener = new(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);\r\n        _listener.Bind(new IPEndPoint(IPAddress.Loopback, 0));\r\n        _listener.Listen(1);\r\n\r\n        Task&lt;Socket&gt; accept = _listener.AcceptAsync();\r\n        _client = new(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);\r\n        await _client.ConnectAsync(_listener.LocalEndPoint!);\r\n        _server = await accept;\r\n        _echoLoop = Task.Run(EchoLoop);\r\n    }\r\n\r\n    [GlobalCleanup]\r\n    public async Task Cleanup()\r\n    {\r\n        _client.Dispose();\r\n\r\n        try\r\n        {\r\n            await _echoLoop;\r\n        }\r\n        catch { }\r\n\r\n        _server.Dispose();\r\n        _listener.Dispose();\r\n    }\r\n\r\n    [Benchmark(OperationsPerInvoke = OperationsPerInvoke)]\r\n    public void SynchronousRoundTripAfterConnectAsync()\r\n    {\r\n        for (int i = 0; i &lt; OperationsPerInvoke; i++)\r\n        {\r\n            _client.Send(_buffer);\r\n            _client.Receive(_buffer);\r\n        }\r\n    }\r\n\r\n    private void EchoLoop()\r\n    {\r\n        var buffer = new byte[1];\r\n        while (_server.Receive(buffer) != 0)\r\n            _server.Send(buffer);\r\n    }\r\n}<\/code><\/pre>\n<p>The background socket echoes each byte. When the client calls <code>Receive<\/code>\nbefore the reply is available, .NET 10 must emulate the wait with the Unix\nsocket poller, whereas .NET 11 can wait in the native blocking <code>recv<\/code> call.<\/p>\n<table>\n<thead>\n<tr>\n<th>Method<\/th>\n<th>Runtime<\/th>\n<th style=\"text-align: right;\">Mean<\/th>\n<th style=\"text-align: right;\">Ratio<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td>SynchronousRoundTripAfterConnectAsync<\/td>\n<td>.NET 10.0<\/td>\n<td style=\"text-align: right;\">199.2 \u03bcs<\/td>\n<td style=\"text-align: right;\">1.00<\/td>\n<\/tr>\n<tr>\n<td>SynchronousRoundTripAfterConnectAsync<\/td>\n<td>.NET 11.0<\/td>\n<td style=\"text-align: right;\">161.6 \u03bcs<\/td>\n<td style=\"text-align: right;\">0.81<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<p>Windows has a different concern around its asynchronous socket operations.\nI mentioned that Windows supplies functions that utilize overlapped I\/O. These APIs, e.g. <code>AcceptEx<\/code>, <code>ConnectEx<\/code>, <code>DisconnectEx<\/code>, and <code>WSARecvMsg<\/code>, are\nextension functions supplied by the installed Winsock provider. .NET looks up\ntheir function pointers dynamically and caches them based on the socket&#8217;s\naddress family, socket type, and protocol. Each <code>Socket<\/code> instance consults that cache the first time it needs one of these functions. In .NET 10, that cache was a small global <code>List&lt;T&gt;<\/code> guarded by a lock. The list rarely contains more than a handful of entries and almost every lookup finds an entry that was initialized earlier, but even those read-only hits acquired the same lock. When many sockets began their first asynchronous operation concurrently, all of those lookups were forced through the lock one at a time. <a href=\"https:\/\/github.com\/dotnet\/runtime\/pull\/124997\">dotnet\/runtime#124997<\/a> changes\nthe cache to a copy-on-write array. The common read path takes a snapshot of\nthe array and scans it without locking. A miss still acquires a lock,\ndouble-checks the latest array, and publishes a new array containing the\nadditional entry. Because entries are added only when a new combination of\naddress family, socket type, and protocol is encountered, misses are rare and\nthe warmed path no longer serializes.<\/p>\n<p>Once you have the socket connection, often the next step is to layer in TLS,\nwith <code>SslStream<\/code>. During client-certificate negotiation, a server can include\nin its <code>CertificateRequest<\/code> message the distinguished names of certificate\nauthorities whose certificates it will accept. <code>SslStream<\/code> turns each encoded\nX.500 name into an <code>X500DistinguishedName<\/code>, ultimately making the names\navailable to certificate-selection logic. In .NET 10, that involved allocating\na <code>byte[]<\/code> for every name. On Windows, the implementation created a span over\nthe native SSPI buffer and then called <code>ToArray<\/code>; on macOS, it copied each\nCore Foundation <code>CFData<\/code> value into a new managed array.\nThe <code>X500DistinguishedName(ReadOnlySpan&lt;byte&gt;)<\/code> constructor has existed since\n.NET 5, but both of these <code>SslStream<\/code> paths predated it and weren&#8217;t updated\nwhen it was added. With\n<a href=\"https:\/\/github.com\/dotnet\/runtime\/pull\/123904\">dotnet\/runtime#123904<\/a>, .NET 11\nremoves those intermediate arrays. Both implementations instead pass a\n<code>ReadOnlySpan&lt;byte&gt;<\/code> over the native encoding directly to the\n<code>X500DistinguishedName<\/code> constructor. The macOS implementation keeps the\n<code>CFData<\/code> handle alive while that span is in use, but the per-authority managed\ncopy is no longer needed.<\/p>\n<p>Once a client certificate has been selected, macOS requires <code>SslStream<\/code> to\npackage the native handles for the leaf certificate and its intermediate\ncertificates into a Core Foundation array. In .NET 10, <code>SslStream<\/code> first\nallocated an <code>IntPtr[]<\/code> large enough for the entire chain, populated it with\nthose handles, and then used the array to create the native <code>CFArray<\/code>.\n<a href=\"https:\/\/github.com\/dotnet\/runtime\/pull\/123905\">dotnet\/runtime#123905<\/a> in .NET 11 changes the interop layer to accept a <code>ReadOnlySpan&lt;IntPtr&gt;<\/code> instead. <code>SslStream<\/code> builds the handle list in a <code>Span&lt;IntPtr&gt;<\/code>, using <code>stackalloc<\/code> for\nchains of up to 128 certificates and falling back to a managed array only for\nlarger chains. Typical certificate chains are far smaller than that, so the\nusual setup path no longer allocates the temporary <code>IntPtr[]<\/code> at all.<\/p>\n<p>A larger Linux change removes copies from the steady-state encrypted-data\npath. <code>SslStream<\/code> uses OpenSSL, and OpenSSL traditionally exchanges data with\nits caller through in-memory buffers known as BIOs. In .NET 10, encryption\nfirst wrote ciphertext into an OpenSSL memory BIO, after which .NET copied it\ninto the buffer to send. Decryption went in the other direction: .NET copied\nreceived ciphertext into a memory BIO, and after OpenSSL decrypted it,\n<code>SslStream<\/code> copied the plaintext from its own buffer into the caller&#8217;s buffer.<\/p>\n<p><a href=\"https:\/\/github.com\/dotnet\/runtime\/pull\/128245\">dotnet\/runtime#128245<\/a> replaces\nthose memory BIOs with a custom BIO that can point directly at managed buffers.\nIn .NET 11, OpenSSL can write encrypted output directly into the buffer\n<code>SslStream<\/code> will send and, in the common case, write decrypted plaintext\ndirectly into the buffer supplied by the caller. The change also combines the\nsetup, OpenSSL operation, and cleanup into one native call rather than four.\nOpenSSL still performs its own internal TLS processing, and <code>SslStream<\/code> retains\na fallback buffer for unusual cases such as TLS alerts or output that doesn&#8217;t\nfit, but the normal application-data path avoids the extra staging copies.<\/p>\n<p>The following benchmark provides a way to reproduce the impact using\nonly public APIs. It establishes a TLS 1.3 connection once, outside the\nmeasurement, and then sends one 16-KB TLS record in each direction:<\/p>\n<pre><code class=\"language-csharp\">\/\/ Linux:\r\n\/\/ dotnet run -c Release -f net10.0 --filter \"*\" --runtimes net10.0 net11.0\r\n\r\nusing System.Net;\r\nusing System.Net.Security;\r\nusing System.Net.Sockets;\r\nusing System.Security.Authentication;\r\nusing System.Security.Cryptography;\r\nusing System.Security.Cryptography.X509Certificates;\r\n\r\nusing BenchmarkDotNet.Attributes;\r\nusing BenchmarkDotNet.Running;\r\n\r\nBenchmarkSwitcher.FromAssembly(typeof(Benchmarks).Assembly).Run(args);\r\n\r\n[HideColumns(\"Job\", \"Error\", \"StdDev\", \"Median\", \"RatioSD\")]\r\npublic class Benchmarks\r\n{\r\n    private const int MessageSize = 16 * 1024;\r\n\r\n    private readonly byte[] _sendBuffer = new byte[MessageSize];\r\n    private readonly byte[] _receiveBuffer = new byte[MessageSize];\r\n    private RSA _rsa = null!;\r\n    private X509Certificate2 _certificate = null!;\r\n    private SslStream _client = null!;\r\n    private SslStream _server = null!;\r\n\r\n    [GlobalSetup]\r\n    public async Task Setup()\r\n    {\r\n        Random.Shared.NextBytes(_sendBuffer);\r\n\r\n        _rsa = RSA.Create(2048);\r\n        var request = new CertificateRequest(\"CN=localhost\", _rsa, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);\r\n        using X509Certificate2 temporary = request.CreateSelfSigned(DateTimeOffset.UtcNow.AddDays(-1), DateTimeOffset.UtcNow.AddDays(1));\r\n        _certificate = X509CertificateLoader.LoadPkcs12(temporary.Export(X509ContentType.Pfx), password: null, X509KeyStorageFlags.Exportable);\r\n\r\n        using TcpListener listener = new(IPAddress.Loopback, 0);\r\n        listener.Start();\r\n\r\n        Socket clientSocket = new(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp)\r\n        {\r\n            NoDelay = true,\r\n        };\r\n        Task&lt;Socket&gt; accept = listener.AcceptSocketAsync();\r\n        await clientSocket.ConnectAsync(listener.LocalEndpoint);\r\n        Socket serverSocket = await accept;\r\n        serverSocket.NoDelay = true;\r\n\r\n        _client = new(new NetworkStream(clientSocket, ownsSocket: true), leaveInnerStreamOpen: false, (_, _, _, _) =&gt; true);\r\n        _server = new(new NetworkStream(serverSocket, ownsSocket: true), leaveInnerStreamOpen: false);\r\n\r\n        using CancellationTokenSource timeout = new(TimeSpan.FromSeconds(30));\r\n        Task clientAuthentication = _client.AuthenticateAsClientAsync(\r\n            new SslClientAuthenticationOptions\r\n            {\r\n                TargetHost = \"localhost\",\r\n                EnabledSslProtocols = SslProtocols.Tls13,\r\n            },\r\n            timeout.Token);\r\n        Task serverAuthentication = _server.AuthenticateAsServerAsync(\r\n            new SslServerAuthenticationOptions\r\n            {\r\n                ServerCertificate = _certificate,\r\n                EnabledSslProtocols = SslProtocols.Tls13,\r\n            },\r\n            timeout.Token);\r\n\r\n        await Task.WhenAll(clientAuthentication, serverAuthentication);\r\n    }\r\n\r\n    [Benchmark]\r\n    public async Task RoundTrip()\r\n    {\r\n        await _client.WriteAsync(_sendBuffer);\r\n        await _server.ReadExactlyAsync(_receiveBuffer);\r\n\r\n        await _server.WriteAsync(_sendBuffer);\r\n        await _client.ReadExactlyAsync(_receiveBuffer);\r\n    }\r\n\r\n    [GlobalCleanup]\r\n    public void Cleanup()\r\n    {\r\n        _client.Dispose();\r\n        _server.Dispose();\r\n        _certificate.Dispose();\r\n        _rsa.Dispose();\r\n    }\r\n}<\/code><\/pre>\n<p>On Ubuntu 24.04 x64 under WSL 2, the 16-KB round trip improves by 16%:<\/p>\n<table>\n<thead>\n<tr>\n<th>Method<\/th>\n<th>Runtime<\/th>\n<th style=\"text-align: right;\">Mean<\/th>\n<th style=\"text-align: right;\">Ratio<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td>RoundTrip<\/td>\n<td>.NET 10.0<\/td>\n<td style=\"text-align: right;\">68.04 \u03bcs<\/td>\n<td style=\"text-align: right;\">1.00<\/td>\n<\/tr>\n<tr>\n<td>RoundTrip<\/td>\n<td>.NET 11.0<\/td>\n<td style=\"text-align: right;\">57.39 \u03bcs<\/td>\n<td style=\"text-align: right;\">0.84<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<p>Moving up the stack, <code>HttpClient<\/code>&#8216;s core HTTP implementation in\n<code>SocketsHttpHandler<\/code> layers protocol processing on top of TLS and the\nunderlying sockets. With <code>AutomaticDecompression<\/code> enabled,\n<code>SocketsHttpHandler<\/code> advertises\nsupported encodings on the request, checks the response&#8217;s final\n<code>Content-Encoding<\/code>, and, when it recognizes gzip, deflate, or Brotli, presents\nan <code>HttpContent<\/code> whose stream decodes the compressed transport bytes as the\ncaller reads them. <a href=\"https:\/\/github.com\/dotnet\/runtime\/pull\/122676\">dotnet\/runtime#122676<\/a> precomputes the combined <code>Accept-Encoding<\/code> value when the handler is created. In the common case where the caller hasn&#8217;t supplied that header, it adds the combined value directly, avoiding an <code>HttpHeaderValueCollection<\/code>, its backing list and header-storage object, and an enumeration of the collection for each enabled algorithm. On the response side, <code>TryGetValues<\/code> avoids materializing a collection when there is no <code>Content-Encoding<\/code>. When decompression is needed, the wrapper takes ownership of the original content-header collection, removes the now-invalid <code>Content-Length<\/code> and the encoding it consumes, and retains any preceding encodings without copying every header into a new collection.<\/p>\n<pre><code class=\"language-csharp\">\/\/ dotnet run -c Release -f net10.0 --filter \"*\" --runtimes net10.0 net11.0\r\n\r\nusing System.IO.Compression;\r\nusing System.Net;\r\nusing System.Net.Sockets;\r\nusing System.Text;\r\nusing BenchmarkDotNet.Attributes;\r\nusing BenchmarkDotNet.Running;\r\n\r\nBenchmarkSwitcher.FromAssembly(typeof(Benchmarks).Assembly).Run(args);\r\n\r\n[MemoryDiagnoser(false)]\r\n[HideColumns(\"Job\", \"Error\", \"StdDev\", \"Median\", \"RatioSD\",\"Mean\",\"Ratio\")]\r\npublic class Benchmarks\r\n{\r\n    private TcpListener _listener = new(IPAddress.Loopback, 0);\r\n    private CancellationTokenSource _cts = new();\r\n    private Task _server = null!;\r\n    private HttpClient _client = null!;\r\n\r\n    [GlobalSetup]\r\n    public async Task Setup()\r\n    {\r\n        _listener.Start();\r\n        _server = ServeAsync(_cts.Token);\r\n        _client = new(new SocketsHttpHandler { AutomaticDecompression = DecompressionMethods.GZip })\r\n        {\r\n            BaseAddress = new Uri($\"http:\/\/127.0.0.1:{((IPEndPoint)_listener.LocalEndpoint).Port}\")\r\n        };\r\n\r\n        await GetAsync();\r\n    }\r\n\r\n    [Benchmark]\r\n    public async Task&lt;int&gt; GetAsync()\r\n    {\r\n        using HttpResponseMessage response = await _client.GetAsync(\"\/\", HttpCompletionOption.ResponseHeadersRead);\r\n        await response.Content.CopyToAsync(Stream.Null);\r\n        return (int)response.StatusCode;\r\n    }\r\n\r\n    [GlobalCleanup]\r\n    public async Task Cleanup()\r\n    {\r\n        _client.Dispose();\r\n        _cts.Cancel();\r\n\r\n        try\r\n        {\r\n            await _server;\r\n        }\r\n        catch { }\r\n\r\n        _listener.Stop();\r\n        _cts.Dispose();\r\n    }\r\n\r\n    private async Task ServeAsync(CancellationToken cancellationToken)\r\n    {\r\n        byte[] body = Compress(new byte[1024]);\r\n        byte[] headers = Encoding.ASCII.GetBytes(\r\n            $\"HTTP\/1.1 200 OK\\r\\nContent-Encoding: gzip\\r\\n\" +\r\n            $\"Content-Length: {body.Length}\\r\\n\\r\\n\");\r\n\r\n        while (true)\r\n        {\r\n            using TcpClient connection = await _listener.AcceptTcpClientAsync(cancellationToken);\r\n            NetworkStream stream = connection.GetStream();\r\n            byte[] request = new byte[4096];\r\n\r\n            while (await ReadRequestAsync(stream, request, cancellationToken))\r\n            {\r\n                await stream.WriteAsync(headers, cancellationToken);\r\n                await stream.WriteAsync(body, cancellationToken);\r\n            }\r\n        }\r\n    }\r\n\r\n    private static async Task&lt;bool&gt; ReadRequestAsync(Stream stream, byte[] buffer, CancellationToken cancellationToken)\r\n    {\r\n        int length = 0;\r\n        while (length &lt; buffer.Length)\r\n        {\r\n            int read = await stream.ReadAsync(buffer.AsMemory(length), cancellationToken);\r\n            if (read == 0)\r\n                return false;\r\n\r\n            length += read;\r\n            if (buffer.AsSpan(0, length).IndexOf(\"\\r\\n\\r\\n\"u8) &gt;= 0)\r\n            {\r\n                return true;\r\n            }\r\n        }\r\n\r\n        throw new InvalidOperationException(\"Request headers are too large.\");\r\n    }\r\n\r\n    private static byte[] Compress(byte[] data)\r\n    {\r\n        using MemoryStream output = new();\r\n        using (GZipStream gzip = new(output, CompressionLevel.SmallestSize, leaveOpen: true))\r\n        {\r\n            gzip.Write(data);\r\n        }\r\n\r\n        return output.ToArray();\r\n    }\r\n}<\/code><\/pre>\n<table>\n<thead>\n<tr>\n<th>Method<\/th>\n<th>Runtime<\/th>\n<th style=\"text-align: right;\">Allocated<\/th>\n<th style=\"text-align: right;\">Alloc Ratio<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td>GetAsync<\/td>\n<td>.NET 10.0<\/td>\n<td style=\"text-align: right;\">3.44 KB<\/td>\n<td style=\"text-align: right;\">1.00<\/td>\n<\/tr>\n<tr>\n<td>GetAsync<\/td>\n<td>.NET 11.0<\/td>\n<td style=\"text-align: right;\">2.87 KB<\/td>\n<td style=\"text-align: right;\">0.83<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<p><code>SocketsHttpHandler<\/code> saw other improvements. HTTP content often consists of a single value, but sometimes a request or response needs to carry several independent pieces together in one body. Multipart content provides that packaging. For example, an HTML form submission might contain a few text fields and a file; each becomes a separate part with its own headers and content, while the collection of parts is sent as one HTTP message body. The receiver needs to know where one part ends and the next begins, so the\nmessage uses a boundary: a token chosen to be unlikely to occur in the content\nitself. In .NET 10,\n<code>MultipartContent<\/code> retained the boundary as a string. Each time the content was\nserialized, it rebuilt the opening and closing delimiter strings, encoded them\ninto bytes, and separately wrote the pieces of the delimiters between parts.\nIn .NET 11, <a href=\"https:\/\/github.com\/dotnet\/runtime\/pull\/124963\">dotnet\/runtime#124963<\/a> instead constructs and\nencodes the opening and closing delimiters once, when the <code>MultipartContent<\/code> is\ncreated. The serialization\npaths can then reuse and directly write those cached bytes.<\/p>\n<pre><code class=\"language-csharp\">\/\/ dotnet run -c Release -f net10.0 --filter \"*\" --runtimes net10.0 net11.0\r\n\r\nusing System.Net.Http;\r\nusing BenchmarkDotNet.Attributes;\r\nusing BenchmarkDotNet.Running;\r\n\r\nBenchmarkSwitcher.FromAssembly(typeof(Benchmarks).Assembly).Run(args);\r\n\r\n[MemoryDiagnoser(false), HideColumns(\"Job\", \"Error\", \"StdDev\", \"Median\", \"RatioSD\")]\r\npublic class Benchmarks\r\n{\r\n    private readonly MultipartContent _content = new(\"mixed\", \"net11-boundary\");\r\n\r\n    [Benchmark]\r\n    public Task Serialize() =&gt; _content.CopyToAsync(Stream.Null);\r\n}<\/code><\/pre>\n<table>\n<thead>\n<tr>\n<th>Method<\/th>\n<th>Runtime<\/th>\n<th>Mean<\/th>\n<th>Ratio<\/th>\n<th>Allocated<\/th>\n<th>Alloc Ratio<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td>Serialize<\/td>\n<td>.NET 10.0<\/td>\n<td>78.49 ns<\/td>\n<td>1.00<\/td>\n<td>296 B<\/td>\n<td>1.00<\/td>\n<\/tr>\n<tr>\n<td>Serialize<\/td>\n<td>.NET 11.0<\/td>\n<td>41.90 ns<\/td>\n<td>0.53<\/td>\n<td>64 B<\/td>\n<td>0.22<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<p>Several allocation reductions remove collections created only to populate or inspect another collection. For example, <a href=\"https:\/\/github.com\/dotnet\/runtime\/pull\/122677\">dotnet\/runtime#122677<\/a> writes HTTP\/3 trailers directly into the final <code>HttpResponseHeaders<\/code> collection, eliminating a temporary <code>List<\/code> of tuples. Trailers are headers sent after the response body, commonly carrying information such as checksums that isn&#8217;t known when the initial headers are written.<\/p>\n<p>Other paths only need a transient view over existing storage.\n<a href=\"https:\/\/github.com\/dotnet\/runtime\/pull\/131142\">dotnet\/runtime#131142<\/a> has\n<code>SocketsHttpHandler<\/code> inspect available HTTP\/2 and HTTP\/3 connections through\nspans, avoiding a copy of each list to an array during idle-connection\neviction. <a href=\"https:\/\/github.com\/dotnet\/runtime\/pull\/123034\">dotnet\/runtime#123034<\/a>\nsimilarly changes <code>HeaderUtilities.DumpHeaders<\/code>, which is used as part of\n<code>ToString<\/code> on header collections, to take a\n<code>params ReadOnlySpan&lt;HttpHeaders?&gt;<\/code>, removing a small array allocation from\n<code>HttpRequestMessage.ToString()<\/code> and <code>HttpResponseMessage.ToString()<\/code>.<\/p>\n<p>Improvements in .NET 11 also show up for <code>Uri<\/code>. Consider <code>https:\/\/user@example.com:8443\/files\/report%20Q3?q=%E4%BD%A0%E5%A5%BD#summary<\/code>.\nBefore <code>Uri<\/code> can expose <code>Scheme<\/code>, <code>UserInfo<\/code>, <code>Host<\/code>, <code>Port<\/code>, <code>AbsolutePath<\/code>,\n<code>Query<\/code>, and <code>Fragment<\/code>, it first locates delimiters such as <code>:<\/code>, <code>\/<\/code>, <code>@<\/code>,\n<code>?<\/code>, and <code>#<\/code>. It then validates each delimited component. ASCII can usually\nremain as-is, <code>%20<\/code> needs unescaping or preservation according to the\ncomponent, and the percent-encoded UTF-8 in the query needs decoding and\nUnicode-aware canonicalization. With <a href=\"https:\/\/github.com\/dotnet\/runtime\/pull\/124433\">dotnet\/runtime#124433<\/a>, .NET 11 uses <code>IndexOfAny<\/code> and <code>SearchValues<\/code> for more of the delimiter-finding work, examining long spans a vector at a time rather than character by character. And once the component boundaries are known, <a href=\"https:\/\/github.com\/dotnet\/runtime\/pull\/119435\">dotnet\/runtime#119435<\/a> replaces repeated reserved-character and unsafe-character tests with a single optimized <code>SearchValues<\/code> lookup.<\/p>\n<pre><code class=\"language-csharp\">\/\/ dotnet run -c Release -f net10.0 --filter \"*UriScanningBenchmarks*\" --runtimes net10.0 net11.0\r\n\r\nusing BenchmarkDotNet.Attributes;\r\nusing BenchmarkDotNet.Running;\r\n\r\nBenchmarkSwitcher.FromAssembly(typeof(UriScanningBenchmarks).Assembly).Run(args);\r\n\r\n[MemoryDiagnoser(false)]\r\n[HideColumns(\"Job\", \"Error\", \"StdDev\", \"Median\", \"RatioSD\")]\r\npublic class UriScanningBenchmarks\r\n{\r\n    private readonly string _longHost = $\"https:\/\/{new string('a', 64)}.example.com\/path\";\r\n    private readonly string _escapedAscii =\r\n        \"https:\/\/example.com\/\" +\r\n        string.Concat(Enumerable.Range('a', 26).Select(i =&gt; $\"%{i:X2}\"));\r\n\r\n    [Benchmark]\r\n    public Uri LongHost() =&gt; new(_longHost);\r\n\r\n    [Benchmark]\r\n    public Uri EscapedAscii() =&gt; new(_escapedAscii);\r\n}<\/code><\/pre>\n<table>\n<thead>\n<tr>\n<th>Method<\/th>\n<th>Runtime<\/th>\n<th>Mean<\/th>\n<th>Ratio<\/th>\n<th>Allocated<\/th>\n<th>Alloc Ratio<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td>LongHost<\/td>\n<td>.NET 10.0<\/td>\n<td>218.1 ns<\/td>\n<td>1.00<\/td>\n<td>56 B<\/td>\n<td>1.00<\/td>\n<\/tr>\n<tr>\n<td>LongHost<\/td>\n<td>.NET 11.0<\/td>\n<td>112.9 ns<\/td>\n<td>0.52<\/td>\n<td>56 B<\/td>\n<td>1.00<\/td>\n<\/tr>\n<tr>\n<td>EscapedAscii<\/td>\n<td>.NET 10.0<\/td>\n<td>442.6 ns<\/td>\n<td>1.00<\/td>\n<td>448 B<\/td>\n<td>1.00<\/td>\n<\/tr>\n<tr>\n<td>EscapedAscii<\/td>\n<td>.NET 11.0<\/td>\n<td>208.1 ns<\/td>\n<td>0.47<\/td>\n<td>368 B<\/td>\n<td>0.82<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<p>After finding a component, <code>Uri<\/code> checks whether its text is already in\ncanonical form or needs to be escaped or normalized. Letters and digits are\nby far the most common characters, but in .NET 10 they still flowed through\nthe more general character tests. Some callers could also repeat a\ncanonicalization check whose answer parsing had already established.\n<a href=\"https:\/\/github.com\/dotnet\/runtime\/pull\/121270\">dotnet\/runtime#121270<\/a> adds a\nfast path for ASCII letters and digits and records the earlier result so that\n.NET 11 can avoid performing the same check again.<\/p>\n<pre><code class=\"language-csharp\">\/\/ dotnet run -c Release -f net10.0 --filter \"*UriCanonicalizationBenchmarks*\" --runtimes net10.0 net11.0\r\n\r\nusing BenchmarkDotNet.Attributes;\r\nusing BenchmarkDotNet.Running;\r\n\r\nBenchmarkSwitcher.FromAssembly(typeof(UriCanonicalizationBenchmarks).Assembly).Run(args);\r\n\r\n[HideColumns(\"Job\", \"Error\", \"StdDev\", \"Median\", \"RatioSD\")]\r\npublic class UriCanonicalizationBenchmarks\r\n{\r\n    private const string Address = \"https:\/\/example.com\/api\/items\/42?view=summary#details\";\r\n\r\n    [Benchmark]\r\n    public Uri Parse() =&gt; new(Address);\r\n}<\/code><\/pre>\n<table>\n<thead>\n<tr>\n<th>Method<\/th>\n<th>Runtime<\/th>\n<th>Mean<\/th>\n<th>Ratio<\/th>\n<th>Allocated<\/th>\n<th>Alloc Ratio<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td>Parse<\/td>\n<td>.NET 10.0<\/td>\n<td>61.86 ns<\/td>\n<td>1.00<\/td>\n<td>56 B<\/td>\n<td>1.00<\/td>\n<\/tr>\n<tr>\n<td>Parse<\/td>\n<td>.NET 11.0<\/td>\n<td>44.64 ns<\/td>\n<td>0.72<\/td>\n<td>56 B<\/td>\n<td>1.00<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<p>Non-ASCII input can require several parts of a URI to be normalized. For\nexample, Unicode characters may need to be preserved or percent-encoded\ndifferently depending on whether they occur in the path, query, or fragment.\nIn .NET 10, parsing and rebuilding were interleaved: <code>Uri<\/code> normalized each of\nthose components separately and repeatedly extended its stored string as it\nwent. In addition to making the offset bookkeeping complicated, those\nindividual normalization results and string concatenations could create\nroughly five temporary strings. In .NET 11, <a href=\"https:\/\/github.com\/dotnet\/runtime\/pull\/122038\">dotnet\/runtime#122038<\/a> separates\nthat rebuilding work from the subsequent validation. <code>Uri<\/code>\nnormalizes the path, query, and fragment into one builder, creates the final\nstring once, and then validates the component boundaries in that completed\nstring. The host is still handled separately, but the remaining components no\nlonger each produce intermediate strings.<\/p>\n<pre><code class=\"language-csharp\">\/\/ dotnet run -c Release -f net10.0 --filter \"*UriNormalizationBenchmarks*\" --runtimes net10.0 net11.0\r\n\r\nusing BenchmarkDotNet.Attributes;\r\nusing BenchmarkDotNet.Running;\r\n\r\nBenchmarkSwitcher.FromAssembly(typeof(UriNormalizationBenchmarks).Assembly).Run(args);\r\n\r\n[MemoryDiagnoser(false)]\r\n[HideColumns(\"Job\", \"Error\", \"StdDev\", \"Median\", \"RatioSD\")]\r\npublic class UriNormalizationBenchmarks\r\n{\r\n    private const string Address =\r\n        \"https:\/\/dot.net\/abc\/defghijklmno\/pqrstuv\/wxyz\" +\r\n        \"?arch=x64&amp;os=linux&amp;type=release#hello\\uD83C\\uDF49\";\r\n\r\n    [Benchmark]\r\n    public Uri Parse() =&gt; new(Address);\r\n}<\/code><\/pre>\n<table>\n<thead>\n<tr>\n<th>Method<\/th>\n<th>Runtime<\/th>\n<th>Mean<\/th>\n<th>Ratio<\/th>\n<th>Allocated<\/th>\n<th>Alloc Ratio<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td>Parse<\/td>\n<td>.NET 10.0<\/td>\n<td>547.6 ns<\/td>\n<td>1.00<\/td>\n<td>936 B<\/td>\n<td>1.00<\/td>\n<\/tr>\n<tr>\n<td>Parse<\/td>\n<td>.NET 11.0<\/td>\n<td>386.9 ns<\/td>\n<td>0.71<\/td>\n<td>432 B<\/td>\n<td>0.46<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<h2>JSON<\/h2>\n<p>JSON readers and writers spend much of their time scanning text: writers look\nfor characters that need escaping, while readers look for whitespace and token\nboundaries. .NET 11 makes several of those scans more efficient.<\/p>\n<p>When using the default encoder, <code>Utf8JsonWriter<\/code> needs to locate characters\nsuch as quotation marks and control characters that can&#8217;t be copied directly\ninto JSON. In .NET 10, that search was routed through\n<code>JavaScriptEncoder.Default<\/code>. <a href=\"https:\/\/github.com\/dotnet\/runtime\/pull\/129781\">dotnet\/runtime#129781<\/a>\ninstead gives .NET 11 precomputed <code>SearchValues<\/code> sets for the default escaping\nrules, allowing the writer to search the input directly. Once it finds a character to escape, the writer must emit a sequence such as\n<code>\\\"<\/code> or <code>\\u0022<\/code>. In .NET 10, the escaping helper received the entire\nremaining destination and performed repeated bounds checks as it wrote each\nbyte or character. <a href=\"https:\/\/github.com\/dotnet\/runtime\/pull\/129803\">dotnet\/runtime#129803<\/a>\npasses only the range known to be writable. The JIT can then prove once that\nthe escape fits and remove the checks from the individual stores.<\/p>\n<pre><code class=\"language-csharp\">\/\/ dotnet run -c Release -f net10.0 --filter \"*JsonWriterBenchmarks*\" --runtimes net10.0 net11.0\r\n\r\nusing System.Text.Json;\r\nusing BenchmarkDotNet.Attributes;\r\nusing BenchmarkDotNet.Running;\r\n\r\nBenchmarkSwitcher.FromAssembly(typeof(JsonWriterBenchmarks).Assembly).Run(args);\r\n\r\n[HideColumns(\"Job\", \"Error\", \"StdDev\", \"Median\", \"RatioSD\")]\r\npublic class JsonWriterBenchmarks\r\n{\r\n    private static readonly string s_fullyEscaped = new('\"', 2_048);\r\n\r\n    [Benchmark]\r\n    public byte[] Write() =&gt; JsonSerializer.SerializeToUtf8Bytes(s_fullyEscaped);\r\n}<\/code><\/pre>\n<table>\n<thead>\n<tr>\n<th>Method<\/th>\n<th>Runtime<\/th>\n<th>Mean<\/th>\n<th>Ratio<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td>Write<\/td>\n<td>.NET 10.0<\/td>\n<td>30.83 \u03bcs<\/td>\n<td>1.00<\/td>\n<\/tr>\n<tr>\n<td>Write<\/td>\n<td>.NET 11.0<\/td>\n<td>7.875 \u03bcs<\/td>\n<td>0.26<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<p>On the reading side, insignificant whitespace is allowed between JSON tokens.\nIndented documents can contain long runs of spaces and newlines, and in\n.NET 10 <code>Utf8JsonReader<\/code> examined those bytes one at a time.\n<a href=\"https:\/\/github.com\/dotnet\/runtime\/pull\/129701\">dotnet\/runtime#129701<\/a> changes\n<code>SkipWhiteSpace<\/code> to use <code>IndexOfAnyExcept<\/code> with a <code>SearchValues<\/code> set containing\nthe four JSON whitespace bytes. .NET 11 can therefore skip a whole run at\nonce, stopping at the next byte that might begin a token.<\/p>\n<pre><code class=\"language-csharp\">\/\/ dotnet run -c Release -f net10.0 --filter \"*JsonReaderBenchmarks*\" --runtimes net10.0 net11.0\r\n\r\nusing System.Text.Json;\r\nusing BenchmarkDotNet.Attributes;\r\nusing BenchmarkDotNet.Running;\r\n\r\nBenchmarkSwitcher.FromAssembly(typeof(JsonReaderBenchmarks).Assembly).Run(args);\r\n\r\n[HideColumns(\"Job\", \"Error\", \"StdDev\", \"Median\", \"RatioSD\")]\r\npublic class JsonReaderBenchmarks\r\n{\r\n    private static readonly Payload s_value = new(new string('a', 2_048), Enumerable.Range(0, 256).ToArray());\r\n    private static readonly byte[] s_json = JsonSerializer.SerializeToUtf8Bytes(s_value, new JsonSerializerOptions { WriteIndented = true });\r\n\r\n    [Benchmark]\r\n    public int Read()\r\n    {\r\n        Utf8JsonReader reader = new(s_json);\r\n        int tokens = 0;\r\n        while (reader.Read()) tokens++;\r\n        return tokens;\r\n    }\r\n\r\n    private sealed record Payload(string Message, int[] Values);\r\n}<\/code><\/pre>\n<table>\n<thead>\n<tr>\n<th>Method<\/th>\n<th>Runtime<\/th>\n<th>Mean<\/th>\n<th>Ratio<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td>Read<\/td>\n<td>.NET 10.0<\/td>\n<td>7.418 \u03bcs<\/td>\n<td>1.00<\/td>\n<\/tr>\n<tr>\n<td>Read<\/td>\n<td>.NET 11.0<\/td>\n<td>5.945 \u03bcs<\/td>\n<td>0.80<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<h2>Diagnostics<\/h2>\n<p>Creating an <code>Activity<\/code>, polling metrics, and logging all add overhead beyond what\nthe application is otherwise trying to accomplish. That cost is deliberately\npaid to make production systems understandable, but observability code also\nsits on paths that can execute for every request, dependency call, or log\nevent. Small fixed costs there can really add up, and disabled or\nunobserved instrumentation needs to be &#8220;pay for play&#8221; so applications don&#8217;t\nincur meaningful costs for diagnostics they aren&#8217;t currently collecting.<\/p>\n<p>Let&#8217;s start with distributed tracing. A trace follows a request as it travels\nthrough an application and potentially across multiple services. Each\noperation along the way can be represented by an <code>Activity<\/code>; the activities\nhave their own span IDs, but share a trace ID that lets a tracing system\ncorrelate them as parts of the same request. The W3C Trace Context standard\ndefines how those identifiers are carried between services, including in an\nHTTP <code>traceparent<\/code> header. Its trace ID is represented as 32 lowercase\nhexadecimal characters, and it can&#8217;t be all zeroes. Applications may need to parse and validate that identifier for every request.\nIn .NET 10, <code>DiagnosticSource<\/code> did so with a loop that checked each character\nboth for whether it was hexadecimal and whether it was non-zero.\nIn .NET 11, <a href=\"https:\/\/github.com\/dotnet\/runtime\/pull\/119673\">dotnet\/runtime#119673<\/a> replaces\nthat loop with two <code>ContainsAnyExcept<\/code> searches: one detects a character\noutside <code>0<\/code>&#8211;<code>9<\/code> and <code>a<\/code>&#8211;<code>f<\/code>, while the other determines whether the entire ID\nis zeroes. Those searches can examine multiple characters at a time. <code>W3CPropagator<\/code> had similar hand-written loops for validating trace-state and\nbaggage characters. In addition to replacing those loops with\n<code>SearchValues&lt;char&gt;<\/code>, the same PR changes baggage encoding to search for the\nfirst character that requires escaping. If there isn&#8217;t one, as is common, it\ncan append the whole value at once rather than checking and appending every\ncharacter individually.<\/p>\n<pre><code class=\"language-csharp\">\/\/ dotnet run -c Release -f net10.0 --filter \"*\" --runtimes net10.0 net11.0\r\n\r\nusing System.Diagnostics;\r\nusing BenchmarkDotNet.Attributes;\r\nusing BenchmarkDotNet.Running;\r\n\r\nBenchmarkSwitcher.FromAssembly(typeof(Benchmarks).Assembly).Run(args);\r\n\r\n[HideColumns(\"Job\", \"Error\", \"StdDev\", \"Median\", \"RatioSD\")]\r\npublic class Benchmarks\r\n{\r\n    private static readonly DistributedContextPropagator s_propagator =\r\n        DistributedContextPropagator.CreateW3CPropagator();\r\n    private readonly Activity _activity = new(\"Test\");\r\n\r\n    [GlobalSetup]\r\n    public void Setup()\r\n    {\r\n        _activity.AddBaggage(\"a\", \"aaaaabbbbbcccccddddd\");\r\n        _activity.Start();\r\n    }\r\n\r\n    [Benchmark]\r\n    public void ExtractTraceParent() =&gt;\r\n        s_propagator.ExtractTraceIdAndState(\r\n            null,\r\n            static (object? carrier, string name, out string? value, out IEnumerable&lt;string&gt;? values) =&gt;\r\n            {\r\n                value = name == \"traceparent\" ? \"00-0af7651916cd43dd8448eb211c80319c-b9c7c989f97918e1-01\" : null;\r\n                values = null;\r\n            },\r\n            out _, out _);\r\n\r\n    [Benchmark]\r\n    public void InjectBaggage() =&gt; s_propagator.Inject(_activity, null, static (object? carrier, string name, string value) =&gt; { });\r\n}<\/code><\/pre>\n<table>\n<thead>\n<tr>\n<th>Method<\/th>\n<th>Runtime<\/th>\n<th>Mean<\/th>\n<th>Ratio<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td>ExtractTraceParent<\/td>\n<td>.NET 10.0<\/td>\n<td>45.41 ns<\/td>\n<td>1.00<\/td>\n<\/tr>\n<tr>\n<td>ExtractTraceParent<\/td>\n<td>.NET 11.0<\/td>\n<td>9.137 ns<\/td>\n<td>0.20<\/td>\n<\/tr>\n<tr>\n<td>InjectBaggage<\/td>\n<td>.NET 10.0<\/td>\n<td>96.69 ns<\/td>\n<td>1.00<\/td>\n<\/tr>\n<tr>\n<td>InjectBaggage<\/td>\n<td>.NET 11.0<\/td>\n<td>47.895 ns<\/td>\n<td>0.50<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<p>Process APIs present a different kind of diagnostics overhead. Launching a\nprocess requires translating managed arguments and environment variables into\nthe representation expected by the operating system, while inspection often\ncrosses into native APIs to retrieve only a small piece of information.<\/p>\n<p>At the lowest level, a new process on Unix receives its command-line arguments\nand environment as <code>argv<\/code> and <code>envp<\/code>. Each is a null-terminated array of\npointers to null-terminated strings; the entries in <code>argv<\/code> are the executable\nand its arguments, while each entry in <code>envp<\/code> has the form <code>key=value<\/code>.\n<code>ProcessStartInfo<\/code>, however, exposes managed strings and a managed environment\ndictionary, so <code>Process.Start<\/code> needs to marshal all of that data into the\nnative representation. In .NET 10, building <code>envp<\/code> first concatenated every key and value into a new managed <code>key=value<\/code> string and collected those strings into an intermediate array. Both <code>argv<\/code> and <code>envp<\/code> were then constructed with a native allocation for the pointer array and another allocation for each UTF-8 string. With\n<a href=\"https:\/\/github.com\/dotnet\/runtime\/pull\/126201\">dotnet\/runtime#126201<\/a>, .NET 11\ninstead makes one pass to count the pointers and calculate the total number of\nUTF-8 bytes required. It then allocates one native block for <code>argv<\/code> and one for\n<code>envp<\/code>, with each block containing both its pointer table and all of its string\ndata, and writes the data directly into those blocks. That avoids the\nintermediate managed strings and array, as well as all of the per-string native\nallocations and frees.<\/p>\n<p>There&#8217;s then the question of how the operating system actually creates the\nprocess. The traditional Unix model uses <code>fork<\/code> to create a child that is\ninitially a logical copy of the parent, followed by <code>exec<\/code> in the child to\nreplace that copy with the requested executable. Copy-on-write means <code>fork<\/code>\ndoesn&#8217;t immediately copy all of the parent&#8217;s memory, but the operating system\nstill needs to duplicate process state and page tables, work that can become\nsignificant for a large, multithreaded application. In .NET 10, <code>Process.Start<\/code> used this <code>fork<\/code>-then-<code>exec<\/code> path on macOS. With <a href=\"https:\/\/github.com\/dotnet\/runtime\/pull\/126063\">dotnet\/runtime#126063<\/a>, .NET 11\nuses <code>posix_spawn<\/code> for the common case. <code>posix_spawn<\/code> asks the operating system\nto create the new process and load its executable as one operation, while\nstill describing the required standard-input\/output\/error redirection, working\ndirectory, and signal state. A launch that requests different user or group\ncredentials still uses <code>fork<\/code> and <code>exec<\/code>, as macOS&#8217;s <code>posix_spawn<\/code> facilities\ncan&#8217;t perform the required <code>setuid<\/code> and <code>setgid<\/code> operations.<\/p>\n<pre><code class=\"language-csharp\">\/\/ Run on Linux and macOS:\r\n\/\/ dotnet run -c Release -f net10.0 --filter \"*ProcessLaunchBenchmarks*\" --runtimes net10.0 net11.0\r\n\r\nusing System.Diagnostics;\r\nusing BenchmarkDotNet.Attributes;\r\nusing BenchmarkDotNet.Running;\r\n\r\nBenchmarkSwitcher.FromAssembly(\r\n    typeof(ProcessLaunchBenchmarks).Assembly).Run(args);\r\n\r\n[MemoryDiagnoser(false), HideColumns(\"Job\", \"Error\", \"StdDev\", \"Median\", \"RatioSD\")]\r\npublic class ProcessLaunchBenchmarks\r\n{\r\n    private readonly ProcessStartInfo _plain = CreateStartInfo();\r\n    private readonly ProcessStartInfo _withEnvironment = CreateStartInfo(includeEnvironment: true);\r\n\r\n    [Benchmark]\r\n    public void StartWithEnvironment() =&gt; StartAndWait(_withEnvironment);\r\n\r\n    [Benchmark]\r\n    public void StartAndWaitForExit() =&gt; StartAndWait(_plain);\r\n\r\n    private static ProcessStartInfo CreateStartInfo(bool includeEnvironment = false)\r\n    {\r\n        ProcessStartInfo psi = new(\"whoami\")\r\n        {\r\n            RedirectStandardOutput = true,\r\n            UseShellExecute = false,\r\n        };\r\n\r\n        if (includeEnvironment)\r\n        {\r\n            for (int i = 0; i &lt; 256; i++)\r\n                psi.Environment[$\"NET11PERF_{i}\"] = new string('x', 32);\r\n        }\r\n\r\n        return psi;\r\n    }\r\n\r\n    private static void StartAndWait(ProcessStartInfo psi)\r\n    {\r\n        using Process process = Process.Start(psi)!;\r\n        process.WaitForExit();\r\n    }\r\n}<\/code><\/pre>\n<p>Process creation dominates the elapsed time in this benchmark, but the\nenvironment-marshalling allocation reduction is clear.<\/p>\n<table>\n<thead>\n<tr>\n<th>Method<\/th>\n<th>Runtime<\/th>\n<th style=\"text-align: right;\">Mean<\/th>\n<th style=\"text-align: right;\">Ratio<\/th>\n<th style=\"text-align: right;\">Allocated<\/th>\n<th style=\"text-align: right;\">Alloc Ratio<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td>StartWithEnvironment<\/td>\n<td>.NET 10.0<\/td>\n<td style=\"text-align: right;\">1.484 ms<\/td>\n<td style=\"text-align: right;\">1.00<\/td>\n<td style=\"text-align: right;\">48.16 KB<\/td>\n<td style=\"text-align: right;\">1.00<\/td>\n<\/tr>\n<tr>\n<td>StartWithEnvironment<\/td>\n<td>.NET 11.0<\/td>\n<td style=\"text-align: right;\">1.435 ms<\/td>\n<td style=\"text-align: right;\">0.97<\/td>\n<td style=\"text-align: right;\">14.48 KB<\/td>\n<td style=\"text-align: right;\">0.30<\/td>\n<\/tr>\n<tr>\n<td><\/td>\n<td><\/td>\n<td style=\"text-align: right;\"><\/td>\n<td style=\"text-align: right;\"><\/td>\n<td style=\"text-align: right;\"><\/td>\n<td style=\"text-align: right;\"><\/td>\n<\/tr>\n<tr>\n<td>StartAndWaitForExit<\/td>\n<td>.NET 10.0<\/td>\n<td style=\"text-align: right;\">1.371 ms<\/td>\n<td style=\"text-align: right;\">1.00<\/td>\n<td style=\"text-align: right;\">16.95 KB<\/td>\n<td style=\"text-align: right;\">1.00<\/td>\n<\/tr>\n<tr>\n<td>StartAndWaitForExit<\/td>\n<td>.NET 11.0<\/td>\n<td style=\"text-align: right;\">1.362 ms<\/td>\n<td style=\"text-align: right;\">0.99<\/td>\n<td style=\"text-align: right;\">14.48 KB<\/td>\n<td style=\"text-align: right;\">0.85<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<p>Once a process is running, a <code>Process<\/code> instance can expose a bunch of information about it. Much of that OS data is gathered and cached together\nin an internal <code>ProcessInfo<\/code> object so that properties needing it can share the work. In .NET 10 on Linux and macOS, however, asking only for <code>ProcessName<\/code>\ntriggered the machinery to populate the whole object and everything on it, which was unnecessarily costly if you only needed the name. <code>Process.ToString()<\/code>\nincludes the process name, so it incurred the same cost. In .NET 11, <a href=\"https:\/\/github.com\/dotnet\/runtime\/pull\/126449\">dotnet\/runtime#126449<\/a> from\n<a href=\"https:\/\/github.com\/tmds\">@tmds<\/a> adds a narrower operating-system query for the\nname. <code>ProcessName<\/code> and <code>ToString()<\/code> can use that to query without\ncollecting the rest of the process metadata.<\/p>\n<pre><code class=\"language-csharp\">\/\/ Run on Linux and macOS:\r\n\/\/ dotnet run -c Release -f net10.0 --filter \"*ProcessNameBenchmarks*\" --runtimes net10.0 net11.0\r\n\r\nusing System.Diagnostics;\r\nusing BenchmarkDotNet.Attributes;\r\nusing BenchmarkDotNet.Running;\r\n\r\nBenchmarkSwitcher.FromAssembly(typeof(ProcessNameBenchmarks).Assembly).Run(args);\r\n\r\n[HideColumns(\"Job\", \"Error\", \"StdDev\", \"Median\", \"RatioSD\")]\r\npublic class ProcessNameBenchmarks\r\n{\r\n    [Benchmark]\r\n    public string GetProcessName()\r\n    {\r\n        using Process process = Process.GetProcessById(Environment.ProcessId);\r\n        return process.ProcessName;\r\n    }\r\n}<\/code><\/pre>\n<table>\n<thead>\n<tr>\n<th>Method<\/th>\n<th>Runtime<\/th>\n<th style=\"text-align: right;\">Mean<\/th>\n<th style=\"text-align: right;\">Ratio<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td>GetProcessName<\/td>\n<td>.NET 10.0<\/td>\n<td style=\"text-align: right;\">332.13 \u03bcs<\/td>\n<td style=\"text-align: right;\">1.00<\/td>\n<\/tr>\n<tr>\n<td>GetProcessName<\/td>\n<td>.NET 11.0<\/td>\n<td style=\"text-align: right;\">11.90 \u03bcs<\/td>\n<td style=\"text-align: right;\">0.04<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<p><code>Process<\/code> can also query processes on another Windows machine. APIs such as\n<code>GetProcesses(string machineName)<\/code> accept a machine name, and the remote path\nuses Windows performance-counter infrastructure to retrieve the information.\nThat support in turn depends on additional components, including remote\nRegistry access. None of that should be necessary for an application that only\nstarts or inspects processes on its own machine. In .NET 10, however, several local-only APIs delegated to overloads that also supported remote machines. For example, <code>GetProcessById(int)<\/code> called the machine-name overload with <code>\".\"<\/code>, and other helpers selected between local and remote implementations at run time. Even when the application always took the local branch, the trimmer saw a call path to both implementations and needed to preserve the remote-process and <code>PerformanceCounter<\/code> code. As a result, even a Native AOT application that did little more than call <code>Process.Start<\/code> could carry that unused support in its executable. In .NET 11, <a href=\"https:\/\/github.com\/dotnet\/runtime\/pull\/126338\">dotnet\/runtime#126338<\/a> gives\nthe local APIs dedicated paths that don&#8217;t reference the remote implementation.\nThe remote implementation is instead reached through a delegate initialized\nonly when an API is actually asked to operate on another machine. Remote\nprocess inspection continues to work, but if an application uses only local\nprocess APIs, .NET 11&#8217;s trimmer can now prove that the remote machinery and its\ndependencies are unreachable and remove them, resulting in significantly smaller binary size.<\/p>\n<pre><code class=\"language-csharp\">\/\/ Add to the csproj's PropertyGroup:\r\n\/\/     &lt;PublishAot&gt;true&lt;\/PublishAot&gt;\r\n\/\/     &lt;InvariantGlobalization&gt;true&lt;\/InvariantGlobalization&gt;\r\n\/\/     &lt;AssemblyName&gt;ProcessSize&lt;\/AssemblyName&gt;\r\n\r\nusing System.Diagnostics;\r\n\r\nusing Process process = Process.Start(new ProcessStartInfo(\"cmd.exe\", \"\/c exit\")\r\n{\r\n    UseShellExecute = false\r\n})!;\r\nprocess.WaitForExit();<\/code><\/pre>\n<p>You can then publish both targets and inspect the resulting executable:<\/p>\n<pre><code class=\"language-powershell\"># dotnet publish -c Release -f net10.0 -r win-x64 -o publish-net10\r\n# dotnet publish -c Release -f net11.0 -r win-x64 -o publish-net11\r\n# Get-Item .\\publish-net10\\ProcessSize.exe, .\\publish-net11\\ProcessSize.exe |\r\n#     ForEach-Object { \"$($_.Directory.Name): $($_.Length) bytes\" }<\/code><\/pre>\n<table>\n<thead>\n<tr>\n<th>Runtime<\/th>\n<th style=\"text-align: right;\">Executable size<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td>.NET 10.0<\/td>\n<td style=\"text-align: right;\">1,599,488 bytes<\/td>\n<\/tr>\n<tr>\n<td>.NET 11.0<\/td>\n<td style=\"text-align: right;\">1,326,080 bytes<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<p>Metrics report numerical information about an application, such as the number\nof requests processed or the current depth of a queue. With\n<code>System.Diagnostics.Metrics<\/code>, a <code>Meter<\/code> creates instruments that produce those\nmeasurements, and a listener such as an OpenTelemetry provider consumes them.\nSome instruments are updated by the application whenever an event occurs. An\nobservable instrument instead registers a callback that computes its current\nvalue when a listener asks to collect it. That pull model is useful for values\nlike queue depth: the application doesn&#8217;t need to record every change, only to\nreport the depth when it&#8217;s observed. The callback for an <code>ObservableGauge&lt;T&gt;<\/code>, <code>ObservableCounter&lt;T&gt;<\/code>, or\n<code>ObservableUpDownCounter&lt;T&gt;<\/code> can return a <code>T<\/code>, a <code>Measurement&lt;T&gt;<\/code>, or an\n<code>IEnumerable&lt;Measurement&lt;T&gt;&gt;<\/code>. A <code>Measurement&lt;T&gt;<\/code> pairs the value with any\nassociated tags, and the enumerable form allows one callback to report\nmultiple tagged values. The first two forms always produce exactly one\nmeasurement. In .NET 10, <code>ObservableInstrument&lt;T&gt;<\/code> nevertheless normalized those\nsingle-value forms into the enumerable model. Every time a listener collected\nthe instrument, it invoked the callback, put the result into a new one-element\n<code>Measurement&lt;T&gt;[]<\/code>, and then enumerated that array to report the value. With\n<a href=\"https:\/\/github.com\/dotnet\/runtime\/pull\/128039\">dotnet\/runtime#128039<\/a> from\n<a href=\"https:\/\/github.com\/unsafePtr\">@unsafePtr<\/a>, .NET 11 recognizes the built-in\nsingle-value forms and sends their result directly to\n<code>MeterListener.NotifyMeasurement<\/code>, avoiding both the array and its enumeration.\nThe enumerable form retains its existing path: the application owns that\nsequence, and it may legitimately contain any number of measurements.<\/p>\n<pre><code class=\"language-csharp\">\/\/ dotnet run -c Release -f net10.0 --filter \"*ObservableBenchmarks*\" --runtimes net10.0 net11.0\r\n\r\nusing System.Diagnostics.Metrics;\r\nusing BenchmarkDotNet.Attributes;\r\nusing BenchmarkDotNet.Running;\r\n\r\nBenchmarkSwitcher.FromAssembly(typeof(ObservableBenchmarks).Assembly).Run(args);\r\n\r\n[MemoryDiagnoser(false), HideColumns(\"Job\", \"Error\", \"StdDev\", \"Median\", \"RatioSD\")]\r\npublic class ObservableBenchmarks\r\n{\r\n    private int _queueLength = 42;\r\n    private Meter _meter = new(\"Sample.Service\");\r\n    private ObservableGauge&lt;int&gt; _gauge = null!;\r\n    private MeterListener _listener = new();\r\n\r\n    [GlobalSetup]\r\n    public void Setup()\r\n    {\r\n        _gauge = _meter.CreateObservableGauge(\"queue.length\", () =&gt; _queueLength);\r\n        _listener.InstrumentPublished = (instrument, listener) =&gt;\r\n        {\r\n            if (instrument.Meter == _meter)\r\n                listener.EnableMeasurementEvents(instrument);\r\n        };\r\n        _listener.SetMeasurementEventCallback&lt;int&gt;( static (instrument, measurement, tags, state) =&gt; { });\r\n        _listener.Start();\r\n    }\r\n\r\n    [GlobalCleanup]\r\n    public void Cleanup()\r\n    {\r\n        _listener.Dispose();\r\n        _meter.Dispose();\r\n    }\r\n\r\n    [Benchmark]\r\n    public void Record() =&gt; _listener.RecordObservableInstruments();\r\n}<\/code><\/pre>\n<table>\n<thead>\n<tr>\n<th>Method<\/th>\n<th>Runtime<\/th>\n<th style=\"text-align: right;\">Mean<\/th>\n<th style=\"text-align: right;\">Ratio<\/th>\n<th style=\"text-align: right;\">Allocated<\/th>\n<th style=\"text-align: right;\">Alloc Ratio<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td>Record<\/td>\n<td>.NET 10.0<\/td>\n<td style=\"text-align: right;\">17.16 ns<\/td>\n<td style=\"text-align: right;\">1.00<\/td>\n<td style=\"text-align: right;\">72 B<\/td>\n<td style=\"text-align: right;\">1.00<\/td>\n<\/tr>\n<tr>\n<td>Record<\/td>\n<td>.NET 11.0<\/td>\n<td style=\"text-align: right;\">4.511 ns<\/td>\n<td style=\"text-align: right;\">0.26<\/td>\n<td style=\"text-align: right;\">&#8211;<\/td>\n<td style=\"text-align: right;\">0<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<p>In previous iterations of Performance Improvements in .NET, I&#8217;ve discussed\n&#8220;false sharing.&#8221; Modern processors move data between memory and their caches\nin fixed-size chunks known as cache lines, commonly 64 bytes. Before a core can\nwrite to a location, it needs exclusive ownership of the cache line containing\nthat location, invalidating copies of the same line held by other cores. That matters even when the cores aren&#8217;t updating the same value. Imagine two\n<code>long<\/code> fields next to each other in memory, with one core repeatedly updating\nthe first and another core repeatedly updating the second. The fields are\nlogically independent, but if they occupy the same cache line, each core&#8217;s\nwrite invalidates the line for the other. Ownership of the line continually\nbounces between the cores, limiting scalability despite there being no\nsharing conceptually. Hence, &#8220;false sharing.&#8221; <code>System.Runtime.Caching.MemoryCache<\/code> maintains performance counters for\noperations such as gets, hits, misses, adds, removes, and trims. In .NET 10,\nthose counters were stored as elements in a small <code>long[]<\/code>. The array header,\nincluding its length, and several unrelated counters could all occupy the same\ncache line. Under load, cores performing different cache operations would\ntherefore contend for ownership of that line as they updated different\ncounters. Accessing a counter through the array also meant loading the array\nlength for a bounds check. <a href=\"https:\/\/github.com\/dotnet\/runtime\/pull\/131470\">dotnet\/runtime#131470<\/a> addresses this in .NET 11 by\nreplacing the array with named fields and laying those fields out across separate\ncache lines. Counters that an operation naturally updates together can remain\ntogether, while unrelated counters are kept apart. That deliberately spends a\nsmall amount of additional memory on padding in order to reduce cache-line\nbouncing under contention, while the named fields also avoid the array bounds\nchecks.<\/p>\n<pre><code class=\"language-csharp\">\/\/ Run separately so each target uses its matching System.Runtime.Caching package:\r\n\/\/ dotnet run -c Release -f net10.0 --filter \"*\"\r\n\/\/ dotnet run -c Release -f net11.0 --filter \"*\"\r\n\r\nusing System.Runtime.Caching;\r\nusing BenchmarkDotNet.Attributes;\r\nusing BenchmarkDotNet.Running;\r\n\r\nBenchmarkSwitcher.FromAssembly(typeof(Benchmarks).Assembly).Run(args);\r\n\r\n[HideColumns(\"Job\", \"Error\", \"StdDev\", \"Median\", \"RatioSD\")]\r\npublic class Benchmarks\r\n{\r\n    private const int ThreadCount = 32;\r\n    private const int TotalOperations = 256_000;\r\n    private readonly MemoryCache _cache = new(\"Benchmark\");\r\n\r\n    [GlobalSetup]\r\n    public void Setup() =&gt; _cache.Set(\"key\", 42, DateTimeOffset.MaxValue);\r\n\r\n    [GlobalCleanup]\r\n    public void Cleanup() =&gt; _cache.Dispose();\r\n\r\n    [Benchmark(OperationsPerInvoke = TotalOperations)]\r\n    public void Get()\r\n    {\r\n        Parallel.For(0, ThreadCount, new ParallelOptions\r\n        {\r\n            MaxDegreeOfParallelism = ThreadCount\r\n        }, _ =&gt;\r\n        {\r\n            for (int i = 0; i &lt; TotalOperations \/ ThreadCount; i++)\r\n                _cache.Get(\"key\");\r\n        });\r\n    }\r\n}<\/code><\/pre>\n<table>\n<thead>\n<tr>\n<th>Method<\/th>\n<th>Runtime<\/th>\n<th>Mean<\/th>\n<th>Ratio<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td>Get<\/td>\n<td>.NET 10.0<\/td>\n<td>75.83 ns<\/td>\n<td>1.00<\/td>\n<\/tr>\n<tr>\n<td>Get<\/td>\n<td>.NET 11.0<\/td>\n<td>51.76 ns<\/td>\n<td>0.68<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<p>Logging is another per-event diagnostics path.\nMicrosoft.Extensions.Logging&#8217;s EventSource provider shrank its cost when the\n<code>JsonMessage<\/code> keyword is on.\n<a href=\"https:\/\/github.com\/dotnet\/runtime\/pull\/131229\">dotnet\/runtime#131229<\/a> reuses a\n<code>[ThreadStatic]<\/code> <code>MemoryStream<\/code> and <code>Utf8JsonWriter<\/code> in\n<code>EventSourceLogger.ToJson<\/code>, avoiding both allocations on every logged event,\nleaving primarily the returned JSON string. Buffers larger than 1 KB aren&#8217;t\nretained on the thread.<\/p>\n<pre><code class=\"language-csharp\">\/\/ Add a FrameworkReference to Microsoft.AspNetCore.App.\r\n\/\/ dotnet run -c Release -f net10.0 --filter \"*\" --runtimes net10.0 net11.0\r\n\r\nusing System.Diagnostics.Tracing;\r\nusing BenchmarkDotNet.Attributes;\r\nusing BenchmarkDotNet.Running;\r\nusing Microsoft.Extensions.Logging;\r\n\r\nBenchmarkSwitcher.FromAssembly(typeof(Benchmarks).Assembly).Run(args);\r\n\r\n[MemoryDiagnoser(false), HideColumns(\"Job\", \"Error\", \"StdDev\", \"Median\", \"RatioSD\")]\r\npublic class Benchmarks\r\n{\r\n    private JsonLoggingListener _listener = null!;\r\n    private ILoggerFactory _factory = null!;\r\n    private ILogger _logger = null!;\r\n\r\n    [GlobalSetup]\r\n    public void Setup()\r\n    {\r\n        _listener = new JsonLoggingListener();\r\n        _factory = LoggerFactory.Create(builder =&gt; builder.AddEventSourceLogger());\r\n        _logger = _factory.CreateLogger(\"Sample\");\r\n    }\r\n\r\n    [GlobalCleanup]\r\n    public void Cleanup()\r\n    {\r\n        _factory.Dispose();\r\n        _listener.Dispose();\r\n    }\r\n\r\n    [Benchmark]\r\n    public void Log() =&gt; _logger.LogInformation(\"Processed {Count} items for {Customer}\", 42, \"Contoso\");\r\n\r\n    private sealed class JsonLoggingListener : EventListener\r\n    {\r\n        protected override void OnEventSourceCreated(EventSource eventSource)\r\n        {\r\n            if (eventSource.Name == \"Microsoft-Extensions-Logging\")\r\n                EnableEvents(eventSource, EventLevel.LogAlways, (EventKeywords)8); \/\/ JsonMessage\r\n        }\r\n    }\r\n}<\/code><\/pre>\n<table>\n<thead>\n<tr>\n<th>Method<\/th>\n<th>Runtime<\/th>\n<th>Mean<\/th>\n<th>Ratio<\/th>\n<th>Allocated<\/th>\n<th>Alloc Ratio<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td>Log<\/td>\n<td>.NET 10.0<\/td>\n<td>506.4 ns<\/td>\n<td>1.00<\/td>\n<td>1.92 KB<\/td>\n<td>1.00<\/td>\n<\/tr>\n<tr>\n<td>Log<\/td>\n<td>.NET 11.0<\/td>\n<td>400.5 ns<\/td>\n<td>0.79<\/td>\n<td>1.15 KB<\/td>\n<td>0.60<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<h2>Cryptography<\/h2>\n<p>ASN.1 is the binary data-description format used by\ncertificates, public and private keys, and many other cryptographic structures.\nIts encodings are nested: reading a sequence produces another reader over the\nsequence&#8217;s contents, which may itself contain more sequences.\n<a href=\"https:\/\/github.com\/dotnet\/runtime\/pull\/125254\">dotnet\/runtime#125254<\/a>\nadds <code>ValueAsnReader<\/code>, a span-based\n<code>ref struct<\/code> counterpart to <code>AsnReader<\/code>. <a href=\"https:\/\/github.com\/dotnet\/runtime\/pull\/125346\">dotnet\/runtime#125346<\/a> further applies that representation to\nselected RSA, PKCS\/CMS, ECC, and X.509 decoders.\nAnd <a href=\"https:\/\/github.com\/dotnet\/runtime\/pull\/125528\">dotnet\/runtime#125528<\/a> carries it through generated\nkey loaders so parsing layers can pass views of the original data by reference\nrather than wrapping the same bytes in new reader objects.<\/p>\n<pre><code class=\"language-csharp\">\/\/ dotnet run -c Release -f net11.0 --filter \"*\"\r\n\r\nusing BenchmarkDotNet.Running;\r\n\r\nusing System.Formats.Asn1;\r\nusing System.Runtime.CompilerServices;\r\nusing BenchmarkDotNet.Attributes;\r\n\r\nBenchmarkSwitcher.FromAssembly(typeof(Benchmarks).Assembly).Run(args);\r\n\r\n[MemoryDiagnoser(false), HideColumns(\"Job\", \"Error\", \"StdDev\", \"Median\", \"RatioSD\")]\r\npublic class Benchmarks\r\n{\r\n    private static readonly byte[] s_der =\r\n    [\r\n        0x30, 0x0F,\r\n        0x30, 0x03, 0x02, 0x01, 0x01,\r\n        0x30, 0x03, 0x02, 0x01, 0x02,\r\n        0x30, 0x03, 0x02, 0x01, 0x03,\r\n    ];\r\n\r\n    [Benchmark(Baseline = true)]\r\n    public int ReadWithEscapingAsnReader()\r\n    {\r\n        AsnReader outer = CreateReader();\r\n        AsnReader sequence = ReadSequence(outer);\r\n        int count = 0;\r\n\r\n        while (sequence.HasData)\r\n        {\r\n            AsnReader child = ReadSequence(sequence);\r\n            _ = child.ReadIntegerBytes();\r\n            child.ThrowIfNotEmpty();\r\n            count++;\r\n        }\r\n\r\n        outer.ThrowIfNotEmpty();\r\n        return count;\r\n    }\r\n\r\n    [Benchmark]\r\n    public int ReadWithValueAsnReader()\r\n    {\r\n        ValueAsnReader outer = new(s_der, AsnEncodingRules.DER);\r\n        ValueAsnReader sequence = outer.ReadSequence();\r\n        int count = 0;\r\n\r\n        while (sequence.HasData)\r\n        {\r\n            ValueAsnReader child = sequence.ReadSequence();\r\n            _ = child.ReadIntegerBytes();\r\n            child.ThrowIfNotEmpty();\r\n            count++;\r\n        }\r\n\r\n        outer.ThrowIfNotEmpty();\r\n        return count;\r\n    }\r\n\r\n    [MethodImpl(MethodImplOptions.NoInlining)]\r\n    private static AsnReader CreateReader() =&gt; new(s_der, AsnEncodingRules.DER);\r\n\r\n    [MethodImpl(MethodImplOptions.NoInlining)]\r\n    private static AsnReader ReadSequence(AsnReader reader) =&gt; reader.ReadSequence();\r\n}<\/code><\/pre>\n<table>\n<thead>\n<tr>\n<th>Method<\/th>\n<th>Mean<\/th>\n<th>Ratio<\/th>\n<th>Allocated<\/th>\n<th>Alloc Ratio<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td>ReadWithEscapingAsnReader<\/td>\n<td>79.81 ns<\/td>\n<td>1.00<\/td>\n<td>240 B<\/td>\n<td>1.00<\/td>\n<\/tr>\n<tr>\n<td>ReadWithValueAsnReader<\/td>\n<td>31.05 ns<\/td>\n<td>0.39<\/td>\n<td>&#8211;<\/td>\n<td>0.00<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<p>Some ASN.1 values add text validation to that parsing work. ASN.1 defines\nseveral text types with restricted character sets. <code>IA5String<\/code> is ASCII, while\n<code>VisibleString<\/code> permits the printable ASCII characters from space through <code>~<\/code>.\nEncoding or decoding one must both copy the data and reject characters outside\nthe allowed range.\n<a href=\"https:\/\/github.com\/dotnet\/runtime\/pull\/131109\">dotnet\/runtime#131109<\/a>\nvectorizes that validation and\ntranscoding for <code>IA5String<\/code> and <code>VisibleString<\/code>, checking and copying multiple\ncharacters at once. <a href=\"https:\/\/github.com\/dotnet\/runtime\/pull\/131170\">dotnet\/runtime#131170<\/a> then applies the same approach\nto big-endian UCS-2 <code>BMPString<\/code>.<\/p>\n<pre><code class=\"language-csharp\">\/\/ dotnet run -c Release -f net10.0 --filter \"*\" --runtimes net10.0 net11.0\r\n\r\nusing System.Formats.Asn1;\r\n\r\nusing BenchmarkDotNet.Attributes;\r\nusing BenchmarkDotNet.Running;\r\n\r\nBenchmarkSwitcher.FromAssembly(typeof(Benchmarks).Assembly).Run(args);\r\n\r\n[HideColumns(\"Job\", \"Error\", \"StdDev\", \"Median\", \"RatioSD\")]\r\npublic class Benchmarks\r\n{\r\n    private static readonly string s_text = new('A', 1024);\r\n\r\n    private readonly char[] _destination = new char[1024];\r\n    private readonly AsnWriter _writer = new(AsnEncodingRules.DER);\r\n    private readonly byte[] _encoded = EncodeText();\r\n\r\n    [Benchmark]\r\n    public int Read()\r\n    {\r\n        AsnDecoder.TryReadCharacterString(\r\n            _encoded,\r\n            _destination,\r\n            AsnEncodingRules.DER,\r\n            UniversalTagNumber.VisibleString,\r\n            out _,\r\n            out int charsWritten);\r\n        return charsWritten;\r\n    }\r\n\r\n    [Benchmark]\r\n    public int Write()\r\n    {\r\n        _writer.Reset();\r\n        _writer.WriteCharacterString(UniversalTagNumber.VisibleString, s_text);\r\n        return _writer.GetEncodedLength();\r\n    }\r\n\r\n    private static byte[] EncodeText()\r\n    {\r\n        AsnWriter writer = new(AsnEncodingRules.DER);\r\n        writer.WriteCharacterString(UniversalTagNumber.VisibleString, s_text);\r\n        return writer.Encode();\r\n    }\r\n}<\/code><\/pre>\n<table>\n<thead>\n<tr>\n<th>Method<\/th>\n<th>Runtime<\/th>\n<th>Mean<\/th>\n<th>Ratio<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td>Read<\/td>\n<td>.NET 10.0<\/td>\n<td>1.243 \u03bcs<\/td>\n<td>1.00<\/td>\n<\/tr>\n<tr>\n<td>Read<\/td>\n<td>.NET 11.0<\/td>\n<td>112.7 ns<\/td>\n<td>0.091<\/td>\n<\/tr>\n<tr>\n<td>Write<\/td>\n<td>.NET 10.0<\/td>\n<td>1.556 \u03bcs<\/td>\n<td>1.00<\/td>\n<\/tr>\n<tr>\n<td>Write<\/td>\n<td>.NET 11.0<\/td>\n<td>152.8 ns<\/td>\n<td>0.098<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<p><a href=\"https:\/\/github.com\/dotnet\/runtime\/pull\/131616\">dotnet\/runtime#131616<\/a> takes that further and extends the approach to the non-contiguous character sets of <code>PrintableString<\/code> and <code>NumericString<\/code>. The validation has more than one accepted range, but it can still classify a vector of characters at a time and fall back to the scalar checks only where necessary:<\/p>\n<pre><code class=\"language-csharp\">\/\/ dotnet run -c Release -f net10.0 --filter \"*\" --runtimes net10.0 net11.0\r\n\r\nusing System.Formats.Asn1;\r\nusing BenchmarkDotNet.Attributes;\r\nusing BenchmarkDotNet.Running;\r\n\r\nBenchmarkSwitcher.FromAssembly(typeof(Benchmarks).Assembly).Run(args);\r\n\r\n[HideColumns(\"Job\", \"Error\", \"StdDev\", \"Median\", \"RatioSD\")]\r\npublic class Benchmarks\r\n{\r\n    private static readonly string s_text = new('A', 1024);\r\n\r\n    private readonly char[] _destination = new char[1024];\r\n    private readonly AsnWriter _writer = new(AsnEncodingRules.DER);\r\n    private readonly byte[] _encoded = EncodeText();\r\n\r\n    [Benchmark]\r\n    public int Read()\r\n    {\r\n        AsnDecoder.TryReadCharacterString(\r\n            _encoded,\r\n            _destination,\r\n            AsnEncodingRules.DER,\r\n            UniversalTagNumber.PrintableString,\r\n            out _,\r\n            out int charsWritten);\r\n        return charsWritten;\r\n    }\r\n\r\n    [Benchmark]\r\n    public int Write()\r\n    {\r\n        _writer.Reset();\r\n        _writer.WriteCharacterString(UniversalTagNumber.PrintableString, s_text);\r\n        return _writer.GetEncodedLength();\r\n    }\r\n\r\n    private static byte[] EncodeText()\r\n    {\r\n        AsnWriter writer = new(AsnEncodingRules.DER);\r\n        writer.WriteCharacterString(UniversalTagNumber.PrintableString, s_text);\r\n        return writer.Encode();\r\n    }\r\n}<\/code><\/pre>\n<table>\n<thead>\n<tr>\n<th>Method<\/th>\n<th>Runtime<\/th>\n<th>Mean<\/th>\n<th>Ratio<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td>Read<\/td>\n<td>.NET 10.0<\/td>\n<td>1.242 \u03bcs<\/td>\n<td>1.00<\/td>\n<\/tr>\n<tr>\n<td>Read<\/td>\n<td>.NET 11.0<\/td>\n<td>263.9 ns<\/td>\n<td>0.21<\/td>\n<\/tr>\n<tr>\n<td>Write<\/td>\n<td>.NET 10.0<\/td>\n<td>1.555 \u03bcs<\/td>\n<td>1.00<\/td>\n<\/tr>\n<tr>\n<td>Write<\/td>\n<td>.NET 11.0<\/td>\n<td>428.3 ns<\/td>\n<td>0.28<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<p>Once all input is available, hashing needn&#8217;t retain reusable state. SHA-1 is no\nlonger suitable for security decisions such as signing new content, but .NET\nstill needs it for compatibility identifiers such as an assembly&#8217;s public-key\ntoken. <a href=\"https:\/\/github.com\/dotnet\/runtime\/pull\/120674\">dotnet\/runtime#120674<\/a>\nadds a one-shot path to the internal implementation used for those non-secret\npurposes. Its hash state, work area, and padding buffer can live on the stack.\n<code>AssemblyName.GetPublicKeyToken()<\/code> now uses the one-shot path as it has\nthe complete public key available for a single operation:<\/p>\n<pre><code class=\"language-csharp\">\/\/ dotnet run -c Release -f net10.0 --filter \"*\" --runtimes net10.0 net11.0\r\n\r\nusing System.Reflection;\r\nusing BenchmarkDotNet.Attributes;\r\nusing BenchmarkDotNet.Running;\r\n\r\nBenchmarkSwitcher.FromAssembly(typeof(Benchmarks).Assembly).Run(args);\r\n\r\n[MemoryDiagnoser(false), HideColumns(\"Job\", \"Error\", \"StdDev\", \"RatioSD\", \"Median\")]\r\npublic class Benchmarks\r\n{\r\n    private static readonly AssemblyName s_an = typeof(object).Assembly.GetName();\r\n\r\n    [Benchmark]\r\n    public byte[]? GetPublicKeyToken() =&gt; ((AssemblyName)s_an.Clone()).GetPublicKeyToken();\r\n}<\/code><\/pre>\n<table>\n<thead>\n<tr>\n<th>Method<\/th>\n<th>Runtime<\/th>\n<th>Mean<\/th>\n<th>Ratio<\/th>\n<th>Allocated<\/th>\n<th>Alloc Ratio<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td>GetPublicKeyToken<\/td>\n<td>.NET 10.0<\/td>\n<td>1.458 \u03bcs<\/td>\n<td>1.00<\/td>\n<td>664 B<\/td>\n<td>1.00<\/td>\n<\/tr>\n<tr>\n<td>GetPublicKeyToken<\/td>\n<td>.NET 11.0<\/td>\n<td>721.4 ns<\/td>\n<td>0.49<\/td>\n<td>296 B<\/td>\n<td>0.45<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<p>AES key wrap is used to encrypt cryptographic keys before they&#8217;re stored or\nsent elsewhere. Wrapping or unwrapping one key requires applying AES many\ntimes. In .NET 10 on Windows and Apple platforms, the implementation performed\neach of those steps through a general-purpose helper that created a native AES\ncipher, processed one block, and then destroyed the cipher. The public <code>Aes<\/code>\nobject could be reused, but internally a single key-wrap operation still\nrepeated that native setup and cleanup, with the number of repetitions growing\nwith the size of the key material. <a href=\"https:\/\/github.com\/dotnet\/runtime\/pull\/129921\">dotnet\/runtime#129921<\/a> from\n<a href=\"https:\/\/github.com\/vcsjones\">@vcsjones<\/a> changes the Windows implementation to\ncreate one native cipher and reuse it for the entire wrap or unwrap operation.\n<a href=\"https:\/\/github.com\/dotnet\/runtime\/pull\/129911\">dotnet\/runtime#129911<\/a> does the same for Apple&#8217;s\nimplementation.<\/p>\n<pre><code class=\"language-csharp\">\/\/ dotnet run -c Release -f net10.0 --filter \"*\" --runtimes net10.0 net11.0\r\n\r\nusing BenchmarkDotNet.Running;\r\n\r\nusing System.Security.Cryptography;\r\nusing BenchmarkDotNet.Attributes;\r\n\r\nBenchmarkSwitcher.FromAssembly(typeof(Benchmarks).Assembly).Run(args);\r\n\r\n[MemoryDiagnoser(false), HideColumns(\"Job\", \"Error\", \"StdDev\", \"Median\", \"RatioSD\")]\r\npublic class Benchmarks\r\n{\r\n    private const int PlaintextLength = 4096;\r\n    private static readonly byte[] s_key = new byte[32]; \/\/ Fixed AES-256 key.\r\n\r\n    private readonly Aes _aes = Aes.Create();\r\n    private byte[] _plaintext = [];\r\n    private byte[] _ciphertext = [];\r\n    private byte[] _encryptDestination = [];\r\n    private byte[] _decryptDestination = [];\r\n\r\n    [GlobalSetup]\r\n    public void Setup()\r\n    {\r\n        _aes.Key = s_key;\r\n\r\n        _plaintext = new byte[PlaintextLength];\r\n        new Random(42).NextBytes(_plaintext);\r\n\r\n        int wrappedLength = Aes.GetKeyWrapPaddedLength(PlaintextLength);\r\n        _ciphertext = new byte[wrappedLength];\r\n        _aes.EncryptKeyWrapPadded(_plaintext, _ciphertext);\r\n        _encryptDestination = new byte[wrappedLength];\r\n        _decryptDestination = new byte[PlaintextLength];\r\n    }\r\n\r\n    [Benchmark]\r\n    public byte[] EncryptKeyWrapPadded()\r\n    {\r\n        _aes.EncryptKeyWrapPadded(_plaintext, _encryptDestination);\r\n        return _encryptDestination;\r\n    }\r\n\r\n    [Benchmark]\r\n    public int DecryptKeyWrapPadded()\r\n    {\r\n        _aes.TryDecryptKeyWrapPadded(_ciphertext, _decryptDestination, out int written);\r\n        return written;\r\n    }\r\n\r\n    [GlobalCleanup]\r\n    public void Cleanup() =&gt; _aes.Dispose();\r\n}<\/code><\/pre>\n<table>\n<thead>\n<tr>\n<th>Method<\/th>\n<th>Runtime<\/th>\n<th>Mean<\/th>\n<th>Ratio<\/th>\n<th>Allocated<\/th>\n<th>Alloc Ratio<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td>EncryptKeyWrapPadded<\/td>\n<td>.NET 10.0<\/td>\n<td>2.497 ms<\/td>\n<td>1.00<\/td>\n<td>264 KB<\/td>\n<td>1.00<\/td>\n<\/tr>\n<tr>\n<td>EncryptKeyWrapPadded<\/td>\n<td>.NET 11.0<\/td>\n<td>111.3 \u03bcs<\/td>\n<td>0.045<\/td>\n<td>88 B<\/td>\n<td>0.00033<\/td>\n<\/tr>\n<tr>\n<td>DecryptKeyWrapPadded<\/td>\n<td>.NET 10.0<\/td>\n<td>2.473 ms<\/td>\n<td>1.00<\/td>\n<td>264 KB<\/td>\n<td>1.00<\/td>\n<\/tr>\n<tr>\n<td>DecryptKeyWrapPadded<\/td>\n<td>.NET 11.0<\/td>\n<td>123.5 \u03bcs<\/td>\n<td>0.050<\/td>\n<td>88 B<\/td>\n<td>0.00033<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<p>Certificate validation can be dominated by work outside the signature math.\nFor example, during revocation checking on Linux, a downloaded certificate\nrevocation list (CRL) is persisted to disk. A later chain build could therefore\navoid the network, but it still needed to open the file, read it, parse the\nencoded CRL, and create a new native handle.\nFor .NET 11, <a href=\"https:\/\/github.com\/dotnet\/runtime\/pull\/123562\">dotnet\/runtime#123562<\/a> adds a\nbounded in-memory cache of parsed CRLs. A repeated lookup can reuse the native\nCRL handle directly, while least-recently-used eviction and GC-assisted aging\nprevent the cache from retaining entries indefinitely.<\/p>\n<p>Authority Information Access (AIA) presents a related problem. A certificate\ncan name a URL from which a missing issuer certificate may be downloaded, and\nmultiple concurrent chain builds may all discover the same missing issuer.\n<a href=\"https:\/\/github.com\/dotnet\/runtime\/pull\/130456\">dotnet\/runtime#130456<\/a> reuses\nthe cache infrastructure so those builds share one asynchronous download\nrather than issuing duplicate requests. Failed downloads aren&#8217;t cached, old\nsuccessful responses are refreshed in the background, and Linux now limits\neach chain build to two AIA downloads, matching Windows and bounding the amount\nof network work one chain can trigger.<\/p>\n<h2>What&#8217;s Next?<\/h2>\n<p>Whew! Several hundred performance improvements later, .NET 11 is indeed one\nlouder. If any of the examples in this post look\nlike code in your applications, please try the latest\n<a href=\"https:\/\/dotnet.microsoft.com\/download\/dotnet\/11.0\">.NET 11 release candidate<\/a>\nand measure your own workloads. If something got faster, we&#8217;d love to hear about it. If something got slower, we&#8217;d also love to hear about it. And if you have ideas for how .NET 12 can be turned up even louder, we&#8217;re all ears.<\/p>\n<p>Happy coding!<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Take a tour through hundreds of performance improvements in .NET 11.<\/p>\n","protected":false},"author":360,"featured_media":60787,"comment_status":"open","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"_acf_changed":false,"footnotes":""},"categories":[685,3009],"tags":[4,108],"class_list":["post-60786","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-dotnet","category-performance","tag-net","tag-performance"],"acf":[],"blog_post_summary":"<p>Take a tour through hundreds of performance improvements in .NET 11.<\/p>\n","_links":{"self":[{"href":"https:\/\/devblogs.microsoft.com\/dotnet\/wp-json\/wp\/v2\/posts\/60786","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/devblogs.microsoft.com\/dotnet\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/devblogs.microsoft.com\/dotnet\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/dotnet\/wp-json\/wp\/v2\/users\/360"}],"replies":[{"embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/dotnet\/wp-json\/wp\/v2\/comments?post=60786"}],"version-history":[{"count":2,"href":"https:\/\/devblogs.microsoft.com\/dotnet\/wp-json\/wp\/v2\/posts\/60786\/revisions"}],"predecessor-version":[{"id":60817,"href":"https:\/\/devblogs.microsoft.com\/dotnet\/wp-json\/wp\/v2\/posts\/60786\/revisions\/60817"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/dotnet\/wp-json\/wp\/v2\/media\/60787"}],"wp:attachment":[{"href":"https:\/\/devblogs.microsoft.com\/dotnet\/wp-json\/wp\/v2\/media?parent=60786"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/dotnet\/wp-json\/wp\/v2\/categories?post=60786"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/dotnet\/wp-json\/wp\/v2\/tags?post=60786"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}