-
Notifications
You must be signed in to change notification settings - Fork 4.8k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Introduce static methods to allocate and throw key exception types. #48573
Comments
Note that this pattern is not a clear win today: #47353 (comment) I think it would make sense to introduce this method to support the planned null checking auto-generated by the compiler. cc @jaredpar @stephentoub |
There are a couple things relative to #47353:
|
I agree. Switching inline In other words, this API sounds like a good idea, but turning it into actual RAM win likely requires work in the JIT to make it as efficient as the inline throw pattern. |
@EgorBo Interested in looking into this? |
@jkotas it's not clear to me what is missing from the JIT side |
@jkotas It might not be a big win from generated code or IL size in applications, but it's a very big win for localization of common validation scenarios. Localization of |
I don't understand. What in ThrowHelper itself needs to be localized, and how does that impact whether code uses a ThrowHelper-like pattern or not? |
If The point of this is less that we need |
That's very different from what's being proposed here. You're actually asking for public APIs for resources Corelib uses? |
@stephentoub looks like I got mixed up between the two proposals when following links and seeing comments like #47353 (comment) |
Could it even be |
For performance reasons it must be a "never returns" type of method to be recognized by jit as a "throw helper", otherwise it might be inlined, e.g. https://sharplab.io/#v2:EYLgxg9gTgpgtADwGwBYA0AXEBDAzgWwB8ABAJgEYBYAKBpuIGYACMpgYSYG8aneme+jFiiYAVABZQIAdwCSAMwByAVwA2qgBQB5YACsYYDEwgBKAb27U+1pgEt5TDRDu4mAOzWqTTDJJnuYaSYAUQQwGAAHDFsINw0TAG5zJgBfZOShYhEJKWl45MsbPl9cgKDQ8KiYuMTktKs+dIbeTOyYXAxybT0DI1MC5Ot7R2cAXlH3TzNmor4cmXiEpgB6ZaZYSABzN1sALxgAEyY8Y6YAIhL/cRhVCJgoM5d3CCNbN1U3w7QmSFUDuGAqggYAA1nUmtZWmJ2hhSN19IZjNNrIVZvM5EpPE5Eis1r4YExpOIIKoCfgYL4IEdbK43h83IcmNdYHY3GBVMoDm9NudLkEGdIzuDqCkgA= |
@EgorBo your example has the test and the throw in the same method. |
@danmoseley see |
I'm not at my computer so I may be missing something, but what I meant is
|
Yep.
It should be enabled for all paths that are guaranteed to end up throwing exception. Cold blocks do not necessarily end up throwing exceptions, they are just executed less frequently, but they may be still pretty performance sensitive and the lazy string optimizations may be unacceptable overhead in some cold blocks. It is what the TODO that you linked to is about. |
Ah, I see - yeah it should work |
What is the exact API proposal here at this point?
Actually, I take that back, we should be able to make the argument So the proposal at this point is just: public class ArgumentNullException
{
public static void ThrowIfNull([NotNull] object? argument, string argumentName);
} ? And then presumably the C# compiler would use this if available to implement |
This proposal is also another form of #20604. |
(It'd be nice if the linker could optionally recognize this as well, to strip out the argument name parameter in a release app build where these are never expected, anyway, and are just for debugging purposes.) |
@jkotas How could introducing these simple non-inlinable methods increase RAM usage? Considering that every current inline use of "throw new ArgumentNullException(nameof(arg))" costs 72 bytes of jitted RAM, cutting that down to a dozen bytes repeated across hundreds of call sites is bound to reduce overall RAM usage. I don't think you want ThrowIfNull since that makes the call site considerably less efficient. I want the null check at the call site, and the failure handling code in the stub helper function. That keeps the mainline code as small and efficient as possible, and moves the slow never-used error handling code out of the way. |
ThrowIfNull will get inlined; how does it make the call site less efficient? |
It costs 72 bytes of jitted RAM, but avoids allocating the string constant eagerly. The string literal constant will be allocated lazily that almost never happens on exception throwing paths. If you change it to non-inlineable helper method, the string constant object will be allocated eagerly that will cost at least:
As I have said, this is can be fixed. And the overhead may be amortized over multiple callsites if they use the same argument name. |
The point of this proposal was to move the code that allocates and throws an exception out of the main hot code path. This is why my code example showed the Throw method as being marked for non-inlinable. The proposal I showed means that in the main code you're left with: A trivial null check And the 72 bytes of code to create and throw an exception is kept out of the hot path. |
I understand. This achieves that: public static void ThrowIfNull([NotNull] object? argument, string argumentName)
{
if (argument is null)
Throw(argumentName);
}
private static void Throw(string argumentName) =>
throw new ArgumentNullException(argumentName); with less IL at each call site but the same generated asm at each call site. ThrowIfNull gets inlined and Throw does not. #nullable enable
using System;
using System.Diagnostics.CodeAnalysis;
public class C {
public void Test1(object arg)
{
if (arg is null)
Throw(nameof(arg));
}
public void Test2(object arg)
{
ThrowIfNull(arg, nameof(arg));
}
private static void ThrowIfNull([NotNull] object? argument, string argumentName)
{
if (argument is null)
Throw(argumentName);
}
private static void Throw(string argumentName) =>
throw new ArgumentNullException(argumentName);
} produces smaller IL for Test2:
and identical asm for Test1 and Test2:
|
My initial proposal above was to introduce methods such as: public class ArgumentNullException : ArgumentException
{
[MethodImpl(MethodImplOptions.NoInlining)]
public static void Throw(string paramName) =>
throw new ArgumentNullException(nameof(paramName));
}
public class ArgumentOutOfRangeException : ArgumentException
{
[MethodImpl(MethodImplOptions.NoInlining)]
public static void Throw(string paramName) =>
throw new ArgumentOutOfRangeException(nameof(paramName));
} which is a general pattern that can apply to any exception type. @stephentoub advocated instead for: public class ArgumentNullException
{
public static void ThrowIfNull([NotNull] object? argument, [CallerArgumentExpression("argument")] string? argumentName = null);
} which is considerably less generic. It doesn't directly translate to other types of exceptions. For example, what would be the equivalent stub for ArgutmentOutOfRangeException? I believe leaving the conditional check in the original code is more generally applicable and would serve the framework better as a generalized pattern. At the call site you end up with: void Foo(object o)
{
_ = o ?? ArgumentNullException.Throw(nameof(o));
} as opposed to: void Foo(object o)
{
ArgumentNullException.ThrowIfNull(o, nameof(o));
} |
I don't think we should have this for most other exception types. In many such situations the arguments to the exception isn't just a const, e.g. you're passing a resource message to ArgumentOutOfRangeException, and if the goal is to keep the calling code lean/small, that resource access should also be part of whatever throw helper you're using to factor out the exception-related code. I called out ArgumentNullException.ThrowIfNull because it's the vastly most common case and generally doesn't suffer from that issue.
FWIW, that's not valid C#, with Throw being void-returning. (And even if it were, I personally find that construction to be cumbersome.) |
namespace System
{
public partial class ArgumentNullException
{
public static void ThrowIfNull([NotNull] object? argument,
[CallerArgumentExpression("argument")] string? paramName = null);
}
} |
For reference: dotnet/csharplang#2145 |
Does |
It will emit a call to a ThrowIfNull method identical to this one but injected as internal into the assembly that's using it. As some point once we better address cross-module inlining with R2R, the compiler might switch to using this method rather than having one per assembly. |
Do we need an issue open to take advantage of this new method throughout our repo? |
No, see #55594 (comment). |
Ah - I missed that, perhaps I searched only issues. I marked #62628 as ready for review. |
EDITED 03/06/2021 by @stephentoub to add revised proposal:
public class ArgumentNullException { + public static void ThrowIfNull([NotNull] object? argument, [CallerArgumentExpression("argument")] string? argumentName = null); }
Background and Motivation
The .NET ecosystem uses extensive argument checking to improve code reliability and predictability. These checks have a substantial impact on code size and often dominate the code for small functions and property setters. This in consumes more RAM, takes more time to JIT, prevents inlining, and most important it causes substantial instruction cache pollution. Ultimately, the presence of these checks slows code down.
Many libraries, including the framework libraries themselves, implement exception throwing helpers to compensate for this bloat. These simple static functions centralize the exception creation and throwing logic. Why not enshrine this pattern in the exception API surface to encourage smaller/faster code, and avoid library authors having to create these stubs themselves?
Proposed API
I propose that the core framework ArgumentXXXException classes be augmented with static functions responsible for both allocating and throwing the exceptions:
There would be one static method corresponding to each constructor signature of the exception type.
This pattern would be warranted for any exception type that has a sufficiently large usage footprint. Certainly the ArgumentXXXException types would qualify for this, perhaps a few others.
Usage Examples
With such functions, the following code
would become
Analyzer and Fixer or Compiler Voodoo
It would be trivial to include an analyzer and associated fixer to upgrade a code base to the new approach, thus encouraging a rapid migration.
Alternatively, the C# compiler could potentially be upgraded to automatically replace canonical uses into calls to the static method which wouldn't require any code changes to yield the perf benefits.
Generated Code
The code required to create and throw an exception costs > 70 bytes of instructions. Here is an example null check compiled in release mode for .NET 5:
The text was updated successfully, but these errors were encountered: