# Deep dive into .NET 11 Performance Improvements #3

In this series, I explore some of the most interesting .NET 11 performance improvements described in Stephen Toub's blog post, [Performance Improvements in .NET 11](https://devblogs.microsoft.com/dotnet/performance-improvements-in-net-11).

Each of my post focuses on one example from the original article while digging a little deeper into an additional aspect that was not covered there. The series covers the following topics:

1. [Allocation free numbers](https://blog.ladeak.net/posts/net11-perf-imp-01.md)
1. [Virtual generic method calls](https://blog.ladeak.net/posts/net11-perf-imp-02.md)
1. *Current post: [Vector zero equality](https://blog.ladeak.net/posts/net11-perf-imp-03.md)*
1. Guid creation on Linux
1. JIT generating implicit-else assembly instructions
1. Guid equality
1. Copy-on-write locking strategy

## Vector zero equality

When computing with vector register, one of the common operation is to extract a bit from each element of a vector data. This operation uses the `ExtractMostSignificantBits` method to check whether any lane matches. This method can be combined with `Popcnt` or `Lzcnt` instructions to count matching lanes or locate a matching lane. The original post says that, in .NET 11, this pattern is recognized on Arm64, avoiding the need to materialize the full scalar mask; instead, the vector mask can be reduced horizontally.

```csharp
public class Benchmarks
{
    private Vector128<int> _value = Vector128.Create(1, -2, 3, -4);

    [Benchmark]
    public bool AnyLessThanExtract() => AnyLessThanCoreExtract(_value, 0);

    [Benchmark]
    public bool AnyLessThanEquality() => AnyLessThanCoreEquality(_value, 0);

    [MethodImpl(MethodImplOptions.NoInlining)]
    private static bool AnyLessThanCoreExtract(Vector128<int> value, int limit) =>
        Vector128.LessThan(value, Vector128.Create(limit))
            .ExtractMostSignificantBits() != 0;

    [MethodImpl(MethodImplOptions.NoInlining)]
    private static bool AnyLessThanCoreEquality(Vector128<int> value, int limit) =>
        Vector128.LessThan(value, Vector128.Create(limit)) != Vector128<int>.Zero;
}
```

This is the optimized assembly code for the `AnyLessThanCoreExtract` method on Arm64 with .NET 11:

```assembly
G_M000_IG01:                ;; offset=0x0000
            stp     fp, lr, [sp, #-0x10]!
            mov     fp, sp

G_M000_IG02:                ;; offset=0x0008
            dup     v16.4s, w0
            cmgt    v16.4s, v16.4s, v0.4s
            umaxv   s16, v16.4s
            umov    w0, v16.s[0]
            cmp     w0, #0
            cset    x0, ne

G_M000_IG03:                ;; offset=0x0020
            ldp     fp, lr, [sp], #0x10
            ret     lr

; Total bytes of code 40
```

In my measurements on a *Snapdragon X 12-core X1E80100* processor running Windows 11, I was not able to measure a significant performance difference before and after the .NET 11 optimization. The optimized code is smaller, which is better for the instruction cache. However, I had two further questions to explore:

1. Does x64 with AVX2 also recognize similar patterns?
1. Is it worth extracting the most significant bits for a zero comparison, or is it better to compare with a zero vector?

This is the optimized code for `AnyLessThanCoreExtract` method on **x64 with AVX2** using .NET 11:

```assembly
; Benchmarks.AnyLessThanCoreExtract(System.Runtime.Intrinsics.Vector128`1<Int32>, Int32)
       vmovd     xmm0,edx
       vpbroadcastd xmm0,xmm0
       vpcmpgtd  xmm0,xmm0,[rcx]
       vmovmskps eax,xmm0
       test      eax,eax
       setne     al
       movzx     eax,al
       ret
; Total bytes of code 26
```

In the x64 case, the assembly code follows a similar pattern and uses `vmovmskps` to move the mask bits into a scalar register. This takes a single instruction, whereas on Arm64 one instruction reduces the mask to the first lane of the vector register and a second moves that lane's value into a scalar register. It is worth noting that the x64 code is smaller.

It is difficult to measure the performance of such a small code sample accurately. In most cases, computer clocks are not granular enough for these types of measurements, and other operations can dominate the measured overhead. Nevertheless, in my measurements on comparable Arm64 and x64 laptops, the x64 laptop appeared to underperform the Arm64 laptop:

```
| Method              | Mean     | Error    | StdDev    |
|-------------------- |---------:|---------:|----------:|
| AnyLessThanExtract  | 4.162 ns | 5.696 ns | 0.3122 ns |
| AnyLessThanEquality | 3.583 ns | 4.849 ns | 0.2658 ns |
```

Finally, is it worth extracting the significant bits for a comparison? In my x64 measurements, it did not appear to be, but as shown above, the results are extremely noisy. For what it is worth, the code for the `AnyLessThanCoreEquality` method is one byte shorter, at only 25 bytes.

## Conclusion

This post focused on the third of many interesting nuances I learned from the [Performance Improvements in .NET 11](https://devblogs.microsoft.com/dotnet/performance-improvements-in-net-11) article. It explores how .NET 11 optimizes vector comparisons against zero on Arm64 with SVE and x64 with AVX2, including the use of horizontal reduction and `vmovmskps`. Although the x64 equality based approach produces slightly smaller code and appeared faster in the measurements, the benchmark results were too noisy to establish a meaningful performance difference.