Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why does SortedList implementation use ThrowHelper instead of throwing directly?

Tags:

c#

Reflector tells me that SortedList uses a ThrowHelper class to throw exceptions instead of throwing them directly, for example:

public TValue this[TKey key]
{
    get
    {
        int index = this.IndexOfKey(key);
        if (index >= 0)
            return this.values[index];
        ThrowHelper.ThrowKeyNotFoundException();
        return default(TValue);
    }

where ThrowKeyNotFoundException does nothing more than just:

throw new KeyNotFoundException();

Note how this requires a duff statement "return default(TValue)" which is unreachable. I must conclude that this is a pattern with benefits large enough to justify this.

What are these benefits?

like image 694
Roman Starkov Avatar asked Feb 18 '09 19:02

Roman Starkov


3 Answers

According to ThrowHelper.cs source code the main purpose is to reduce the JITted code size. Below is a direct copy paste from the link:

// This file defines an internal class used to throw exceptions in BCL code.
// The main purpose is to reduce code size. 
// 
// The old way to throw an exception generates quite a lot IL code and assembly code.
// Following is an example:
//     C# source
//          throw new ArgumentNullException("key", Environment.GetResourceString("ArgumentNull_Key"));
//     IL code:
//          IL_0003:  ldstr      "key"
//          IL_0008:  ldstr      "ArgumentNull_Key"
//          IL_000d:  call       string System.Environment::GetResourceString(string)
//          IL_0012:  newobj     instance void System.ArgumentNullException::.ctor(string,string)
//          IL_0017:  throw
//    which is 21bytes in IL.
// 
// So we want to get rid of the ldstr and call to Environment.GetResource in IL.
// In order to do that, I created two enums: ExceptionResource, ExceptionArgument to represent the
// argument name and resource name in a small integer. The source code will be changed to 
//    ThrowHelper.ThrowArgumentNullException(ExceptionArgument.key, ExceptionResource.ArgumentNull_Key);
//
// The IL code will be 7 bytes.
//    IL_0008:  ldc.i4.4
//    IL_0009:  ldc.i4.4
//    IL_000a:  call       void System.ThrowHelper::ThrowArgumentNullException(valuetype System.ExceptionArgument)
//    IL_000f:  ldarg.0
//
// This will also reduce the Jitted code size a lot. 
like image 179
tbaskan Avatar answered Nov 17 '22 12:11

tbaskan


Look at what ThrowHelper does. It gets resources and stuff for the error messages. In this particular instance, there's no error text, so it seems like it's useless, but their pattern probably requires it, so the developer who wrote it followed the pattern like s/he should.

like image 29
Robert C. Barth Avatar answered Nov 17 '22 13:11

Robert C. Barth


Another interesting aspect is performance. Interestingly enough, a method, which contains a throw statement can be slower even if the exception is not thrown just because JIT prefers not to inline such methods (maybe for the better readability of the call stack). Consider the following example:

private class TestClass
{
    internal int RegularThrow(int value)
    {
        if (value < 0)
            throw new ArgumentOutOfRangeException(nameof(value));
        return value + 1;
    }

    internal int ThrowByHelper(int value)
    {
        if (value < 0)
            Throw.ArgumentOutOfRangeException(Argument.value); // Argument is an enum
        return value + 1;
    }
}

Performance results on my computer:

(See the source link along with some remarks below)

1. ThrowByHelper: average time: 5,24 ms
  #1           5,26 ms
  #2           5,16 ms   <---- Best
  #3           5,31 ms   <---- Worst
  Worst-Best difference: 0,16 ms (3,02%)
2. RegularThrow: average time: 23,51 ms (+18,27 ms / 448,40%)
  #1          23,46 ms
  #2          23,42 ms   <---- Best
  #3          23,65 ms   <---- Worst
  Worst-Best difference: 0,22 ms (0,95%)

Meaning, the method with the explicit throw statement was 4.5 times slower! But...

Interesting Observations:

  • Starting with .NET 4.0 you can apply the [MethodImpl(MethodImplOptions.AggressiveInlining)] attribute, though it does not guarantee anything. For example, in .NET Fiddle inlining appears to be generally disabled, hence both ways have effectively the same performance. See the source code and the completely different results here.
  • In .NET Core 3 (x64 release build on Windows 10) it seems that only exception types derived from ArgumentException prevented inlining (without using any attributes). At least throwing a NotSupportedException or InvalidOperationException directly did not affect the performance negatively.
  • For more complex methods a throw helper does not make any sense anyway. But it can be useful in short performance-critical members.

Redundant/unreachable code issue and code analyzers:

The redundant return statement can be avoided by defining some generic overloads in the ThrowHelper:

// for regular usage:
internal static void ArgumentException(Argument arg, string message) => throw new...

// for expression usage:
internal static T ArgumentException<T>(Argument arg, string message) => throw new...

The latter can be used in a return statement, can spare a break in case blocks and starting with C# 7.0 it can be used the same way as throw expressions:

return value >= 0 ? value + 1 : Throw.ArgumentOutOfRangeException<int>(Argument.value);

Another issue is that ReSharper and FxCop do not recognize the throw helper members and may start emitting false positive warnings. For ReSharper we can use the ContractAnnotation attribute:

// prevents PossibleNullReferenceException, AssignNullToNotNullAttribute and similar false alarms
[ContractAnnotation("=> halt")]
internal static void ArgumentException(Argument arg, string message) => throw new...

Unfortunately for FxCop I did not find a similar solution (and the [DoesNotReturn] attribute apparently does not work) so you should use #pragma warning disable or the SuppressMessage attribute to suppress CA1031, CA1062 and their friends.

like image 2
György Kőszeg Avatar answered Nov 17 '22 14:11

György Kőszeg