Parameter Null Checking in C# 11 and the ThrowHelper pattern

Throwing exceptions in C# seems straightforward enough. You check for a certain situation and throw if it is an exceptional situation. However, just by having the code that throws an exception in your method can be inefficient, even if the exception is never thrown.

Coming in C# 11 is a new feature named Parameter Null Checking. It's available at the time of writing in .NET 7 Preview 1.

This post takes a quick look at Parameter Null Checking and a related micro-optimisation pattern for throwing exceptions called Throw Helpers. The optimisation is used by Parameter Null Checking, as well as many existing projects, including the .NET Runtime itself. The performance gains that can be achieved with this pattern are quite small though, so if you're looking at this post hoping that Throw Helpers will make a noticeable difference to your slow running app, then it'll be worth reading other material that could make much more of a difference.

šŸ¤“ NOTE
This post contains C# snippets and assembly code. You don't need to know what the assembly code is doing; the primary reason for showing it is just to demonstrate the difference in the amount of instructions between the different ways of throwing an exception


Parameter Null Checking #

To check for null parameters, the way we do it today (pre C# 11) is:

void Foo(string s) {
if(s is null) throw new ArgumentNullException(nameof(s));
}

With Parameter Null Checking, it's shortened into two !! on the parameter name:

void Foo(string s!!) {
}

Aside from a shorter syntax, they are semantically the same.

Almost.

Having a throw new in your methods can be inefficient. The inefficiency comes from the fact that a fair amount of assembly code is generated to throw the exception.

You might be thinking "so what, the act of throwing an exception is, in relative terms, one of the slowest operations you can perform".

And you'd be right. However, the code that is generated when you throw an exception this way is generated inline in your method.
Every time your method is run, registers are initialised and prepared for that exception, even if the exception is never thrown.

But there is a pattern that can help both eliminate the extra assembly code instructions, and optimise the code so that registers are not set-up needlessly.

The pattern simply involves using a separate method to throw your exceptions, and is generally called a 'ThrowHelper'. For example, this is a ThrowHelper:

public static class ThrowHelper {
public static void ThrowMissingFooException() => throw MissingFooException();
}

and it is called like this:

public void ProcessBar(Bar bar) {
if(bar.Foo is null) ThrowHelper.ThrowMissingFooException();
}

A ThrowHelper can be a method in a separate class or can just be a method in your type.

Let's see the difference between throwing an exception inline and throwing using a ThrowHelper. The C# code below has three methods, A throws inline, and B and C uses a ThrowHelper:

using System;

public class MyClass {
public void A() {
throw new InvalidOperationException("whatever");
}

public void B() {
ThrowHelper.ThrowInvalidOperationException();
}

public void C() {
ThrowHelper.ThrowInvalidOperationException();
}
}

public static class ThrowHelper {
public static void ThrowInvalidOperationException() {
throw new InvalidOperationException("whatever");
}
}

This generates the following assembly code (see it on sharplab):

(you don't need to understand what it does, just notice the size difference between the methods)

; Core CLR 6.0.121.56705 on x86

MyClass..ctor()
L0000: ret

MyClass.A()
L0000: push esi
L0001: mov ecx, 0x99addfc
L0006: call 0x05d930c0
L000b: mov esi, eax
L000d: mov ecx, 1
L0012: mov edx, 0xe68c030
L0017: call 0x721587c0
L001c: mov edx, eax
L001e: mov ecx, esi
L0020: call System.InvalidOperationException..ctor(System.String)
L0025: mov ecx, esi
L0027: call 0x721519c0
L002c: int3

MyClass.B()
L0000: call dword ptr [0xe68c958]
L0006: int3

MyClass.C()
L0000: call dword ptr [0xe68c958]
L0006: int3

ThrowHelper.ThrowInvalidOperationException()
L0000: push esi
L0001: mov ecx, 0x99addfc
L0006: call 0x05d930c0
L000b: mov esi, eax
L000d: mov ecx, 1
L0012: mov edx, 0xe68c030
L0017: call 0x721587c0
L001c: mov edx, eax
L001e: mov ecx, esi
L0020: call System.InvalidOperationException..ctor(System.String)
L0025: mov ecx, esi
L0027: call 0x721519c0
L002c: int3

You can see that method A has the overhead of setting up and throwing the exception, but methods B and C don't, as they call another method to do it. Obviously, in this simple example, the exception is always thrown, but in reality, your exception would rarely (if ever) be thrown.

If you're like me, and you spotted that kind of repetition in C#, you'd likely perform an ExtractMethod refactoring!

The size difference between the assembly routines is quite noticeable. Even though it's only a few bytes difference, if you have a largish project, with hundreds or thousands of methods, and where a large percentage of them could throw exceptions, then it soon adds up. Also, when you consider generics, where types and methods are generated per type, then the situation is compounded.

Inlining #

šŸ¤“ Inlining?
The JIT can take a method's instructions and repeat it inline everywhere that method is called. This can be more performant at runtime as no jumps are necessary. So you get the syntactic niceness of separation of concerns (difference concerns in different methods), and the runtime performance improvement by having less branching. There are obvious things that the JIT considers when finding candidates to inline (size being one that immediately springs to mind), which this post on RyuJIT performance improvements describes.

Looking at those assembly instructions above, you'd probably noticed that the JIT compiler didn't inline the method that throws the exception (ThrowHelper.ThrowInvalidOperationException).

To explain this, we can take a look at a class named ThrowHelper from the .NET Runtime. It says:

This pattern of easily inlinable "void Throw" routines that stack on top of NoInlining factory methods
is a compromise between older JITs and newer JITs (RyuJIT in Core CLR 1.1.0+ and desktop CLR in 4.6.3+).
This package is explicitly targeted at older JITs as newer runtimes expect to implement Span intrinsically for
best performance.

The aim of this pattern is three-fold
1: Extracting the throw makes the method preforming the throw in a conditional branch smaller and more inlinable
2: Extracting the throw from generic method to non-generic method reduces the repeated codegen size for value types
3a: Newer JITs will not inline the methods that only throw and also recognise them, move the call to cold section
and not add stack prep and unwind before calling https://github.com/dotnet/coreclr/pull/6103
3b: Older JITs will inline the throw itself and move to cold section; but not inline the non-inlinable exception
factory methods - still maintaining advantages 1 & 2

It says that newer JITs, such as RyuJIT recognise methods that only throw exceptions, and it decides not to inline them. This is good in this case, as we only want to prepare for the exception path if that method is called.

Just out of my own curiosity (as I've never looked this deeply at the size of IL before), I created another trivial method to see if that was inlined. Here's the C# code:

using System;

public class MyClass {
public void A() {
WriteSomething();
Console.ReadLine();
}

public static void WriteSomething() {
Console.WriteLine("xx");
}
}

Here's the assembly code:


; Core CLR 6.0.121.56705 on x86

MyClass..ctor()
L0000: ret

MyClass.A()
L0000: push ebp
L0001: mov ebp, esp
====> inlined
šŸ‘‰ L0003: mov ecx, [0x8594a44]
šŸ‘‰ L0009: call System.Console.WriteLine(System.String)
<====
L000e: call System.Console.ReadLine()
L0013: pop ebp
L0014: ret

MyClass.WriteSomething()
šŸ‘† L0000: mov ecx, [0x8594a44]
šŸ‘† L0006: call System.Console.WriteLine(System.String)
L000b: ret

The JIT, in this case, recognised that WriteSomething could be inlined.

But, recall that it didn't inline the method that threw the exception. It treated that exception block as a 'cold block'. A cold block is something that's probably not going to be run. The idea behind this is that the JIT doesn't want to load it to the instruction cache and doesn't want the register allocator to bother preparing arguments in registers, as that may cause spilling. I won't go into detail on registers and spilling here, mostly because I'd only be reciting other material, but there's lots of information online about it and it's not specific to C#, or .NET. I will summarise though, by saying that it's inefficient to set up registers for a call that is never going to happen.

šŸ¤“ Interesting fact
The JIT assumes exceptions won't happen. It won't make special preparation for them as this could impact the non-exception path of execution.

nulls everywhere!! #

Going back to Parameter Null Checking:

public class C {
void Test(string x!!) {
}
}

If we take a look at the assembly code that is generated (again, it's just a cursory look, more to see the size rather than anything else):

; Core CLR 6.0.121.56705 on x86

C.Test(System.String)
L0000: push ebp
L0001: mov ebp, esp
L0003: push eax
L0004: mov [ebp-4], ecx
L0007: cmp dword ptr [0xfacc190], 0
L000e: je short L0015
L0010: call 0x722c4bc0
L0015: mov edx, [0x8535518]
L001b: mov ecx, [ebp-4]
L001e: call dword ptr [0xfacc944]
L0024: nop
L0025: nop
L0026: nop
L0027: mov esp, ebp
L0029: pop ebp
L002a: ret

<PrivateImplementationDetails>.Throw(System.String)
L0000: push ebp
L0001: mov ebp, esp
L0003: sub esp, 8
L0006: xor eax, eax
L0008: mov [ebp-8], eax
L000b: mov [ebp-4], ecx
L000e: cmp dword ptr [0xfacc190], 0
L0015: je short L001c
L0017: call 0x722c4bc0
L001c: mov ecx, 0x9801870
L0021: call 0x05cf30c0
L0026: mov [ebp-8], eax
L0029: mov ecx, [ebp-8]
L002c: mov edx, [ebp-4]
L002f: call System.ArgumentNullException..ctor(System.String)
L0034: mov ecx, [ebp-8]
L0037: call 0x721519c0
L003c: int3

<PrivateImplementationDetails>.ThrowIfNull(System.Object, System.String)
L0000: push ebp
L0001: mov ebp, esp
L0003: sub esp, 8
L0006: mov [ebp-4], ecx
L0009: mov [ebp-8], edx
L000c: cmp dword ptr [0xfacc190], 0
L0013: je short L001a
L0015: call 0x722c4bc0
L001a: cmp dword ptr [ebp-4], 0
L001e: jne short L0028
L0020: mov ecx, [ebp-8]
L0023: call <PrivateImplementationDetails>.Throw(System.String)
L0028: nop
L0029: mov esp, ebp
L002b: pop ebp
L002c: ret

... we can see that it uses the ThrowHelper pattern via the Throw and ThrowIfNull routines.

So, using void Test(string x!!) generates two functions that act as a ThrowHelper.

Performance Improvements? #

I was unable to create a meaningful benchmark to show performance improvements. The super helpful folks on the Runtime Discord server explained that it might be difficult to create cache/TLB pressure in a micro-benchmark.

But J. Zebedee, who knows far more about Benchmark than me, did kindly provide a benchmark. The benchmark shows some real performance improvements (but please note, that the performance improvements are in microseconds, but nonetheless, they are performance improvements):

BenchmarkDotNet=v0.12.1, OS=Windows 10.0.22000
AMD Ryzen 9 5950X, 1 CPU, 32 logical and 16 physical cores
.NET Core SDK=6.0.200
[Host] : .NET Core 6.0.2 (CoreCLR 6.0.222.6406, CoreFX 6.0.222.6406), X64 RyuJIT
Job-WCRXHC : .NET Core 6.0.2 (CoreCLR 6.0.222.6406, CoreFX 6.0.222.6406), X64 RyuJIT
MethodMeanErrorStdDevRatioBranchInstructions/OpBranchMispredictions/Op
BenchInline744.2 Ī¼s35.14 Ī¼s1.93 Ī¼s1.00183,302973
BenchThrowHelper110.7 Ī¼s4.11 Ī¼s0.23 Ī¼s0.1597,844450

The .NET Runtime team said that they found a performance improvement when they replaced a lot of throw new code with !! in the Runtime itself. Hopefully they'll provide some numbers on this improvement.

Summary #

We've seen that any method with a throw new generates instructions. Those instructions make the code slightly bigger and ever so slightly slower. The alternative is a ThrowHelper, which the JIT recognises as any method that only throws an exception. This keeps things separate so that time isn't wasted preparing for a call to throw an exception that will likely never happen.

We've seen that the new C# 11 feature named Parameter Null Checking also uses a ThrowHelper.

I have some reservations about Parameter Null Checking. Not the feature itself (although the syntax is a bit whacky!), but more on the actual usefulness of throwing ArgumentNullException in the first place. I'll likely write about that in the future, but just quickly, I think a more specific application exception should be thrown, e.g. CustomerProcessingException("a null Customer was provided"). ArgumentNullException is OK in a framework, but not much use, and not really recoverable from in anything else.

Please feel free to leave a comment below.

šŸ™šŸ™šŸ™

Since you've made it this far, sharing this article on your favorite social media network would be highly appreciated šŸ’–! For feedback, please ping me on Twitter.

Leave a comment

Comments are moderated, so there may be a short delays before you see it.

Published