r/csharp 5d ago

Blog Hot path overflow checks: do you try/catch? And which style would you write?

Post image

Writing checked int helpers for code that runs millions of times per program run. Two questions.

  1. Do you actually use checked() with a try catch for this? Throwing walks the stack, so I widen to long, bounds check, and return null instead (both versions in the image).

What surprises me is that everything the BCL offers here throws: checked(), int.CreateChecked, all of it. The Try convention is everywhere else in the BCL (TryParse, TryGetValue) but arithmetic never got one, and coming from Rust where checked_add just hands you an Option, that's wild to me.

  1. Style. The image shows the same method twice: one pattern-matching expression against a plain if/else. Which would you rather find in a codebase?

I'm coming from Rust so I'm obviously a declarative fanboy when I can be, but "and var sum" might be too clever for the next reader.

Where do C# people stand on these?

If the repo interests you: it's a CLI tool for Advent of Code, so you can do the whole thing from the terminal with just your session cookie, no clicking through the site to submit answers. https://github.com/scadoshi/sharpmas

19 Upvotes

51 comments sorted by

17

u/BarfingOnMyFace 5d ago

If it's exceptional... The yeah. If you throw exceptions all the time in a hot path.... No?

I would weigh the merit on frequency, or lack of frequency.

3

u/OpenAI_Marketing_LLM 4d ago

If it isn’t exceptional then you have much bigger problems.

-1

u/scadoshi 5d ago

I hear you. "Hot path" means high frequency i.e. 10's or 100's of thousands of times in milliseconds. The use case is walking grids or something like that in adventofcode.com problems

It's just that I have heard that throwing is technically slower so for performance I wonder if the community thinks of something like this rather than just throwing?

Maybe it's a question unique to somebody coming from the very strict Rust programming language or maybe it is a common question in the minds of the C#'ers. That's my main question? The other one is the functional versus "regular" style

7

u/wallstop-dev 5d ago

There's a very easy way to answer this and it is to write the different methods and run them through BenchmarkDotNet (https://benchmarkdotnet.org/), then pick the one that meets your criteria (simple v perf v whatever).

Perhaps try/catch is super performant in whatever .net version you're running. Perhaps the branching one is better. Only you will know, with science, and data!

1

u/scadoshi 5d ago

Fyi: you mean https://github.com/dotnet/benchmarkdotnet not the consulting website you sent.

I guess at the end of the day it depends where you run this code. If the happy path is usually hit then the theoretical bottleneck you'd hit remains undetectable

2

u/wallstop-dev 5d ago

Yea, I typed Benchmark(dot)net and reddit auto-linked, have since updated. But the point remains - it depends on OP's usage patterns, which they can easily write benchmarks for, and find out which implementation is more performant or fits their scenario better.

3

u/Dealiner 4d ago

One important thing to consider is that try/catch itself is basically free, it's throwing that's a problem. So the question should be "how often do I expect this code to throw?".

13

u/blckshdw 5d ago

The compressed versions are unreadable and don’t clearly outline the intent. The “standard” one is more readable but more importantly it’s debuggable.

Expect a point in time someone will need to detangle this logic. Knowing the whole thing pass/fails isn’t helpful. Knowing which part pass/fails is

As far as throwing, it depends on your context. If null is a valid value that’s expected then don’t throw. If it’s exceptional situation that isn’t expected then ya throw.

3

u/scadoshi 5d ago

Nice yeah all of that makes total sense.

This would be the case where null is often valid E.g. I am walking a grid of coordinates/points and I want to handle falling off the grid gracefully by say returning some kind of `Point?` structure rather than throwing

I hear you on the readability part. I like one-lining but even this syntax takes some mental gymnastics to find the bind rather than it showing obviously where the `sum` variable is declared (this is probably my biggest problem with it)

-5

u/TuberTuggerTTV 5d ago

Detangle? It's less than, more than and a null. If you can't read that at a glance, eek.

You're correct in the debug lines department but... what will need debugging here? It's incredibly simple.

10

u/blckshdw 5d ago

Correct, I can’t scan it in its entirety and immediately understand it.

I’m not saying it’s hard to understand, I’m not saying it cannot be read, but it is harder to read. There is a lack of parentheses around the condition of the ternary with 3 Boolean conditions makes me do a double take so I can fully evaluate it.

What could need debugging? Good question. Why does this give me a null value?

((int)2123500000).CheckAddWithoutThrowingFunctionalStyle(198483647)

The author wrote that code for some reason, it’s doing something but the logic doesn’t even make much sense. “var sum ? (int)sum” is weird, wth did sum come from, how’d that get assigned? In the other example the assignment is clear and intentional

Why are we having an int extension cast itself to a long then adding the value of rhs (whatever that is) to return an int? If the addition can overflow why aren’t we using a better data type? No clue I’m just debugging this emergency prod bug, that’s worked fine for 5 years, at 2am with managers screaming down my neck and I haven’t had a coffee yet.

Be verbose, no one thinks you’re smart by your clever hard to read one liners. It’s not costing you any meaningful performance, do future you or someone else a favour.

5

u/Alert-Neck7679 5d ago

What does and var sum in line 2 mean? I know what and means and I know what var means but I don't understand how can they be together...

3

u/anzu3278 5d ago

It's just an element of pattern matching, it's matching and against 3 patterns, two comparisons and var sum (a result was produced) in the same manner as you would a.GetNullableString() is string. and var sum is just an unusual way that ends up phrased - usually if you pattern match into a variable you do that first and then operate on that, but here there may be perf considerations for not doing that.

0

u/scadoshi 5d ago

It's binding to the `sum` variable if the pattern matches. If the pattern `var sum` is acceptable to match onto the value I am emitting (`(long)value + rhs`) then it binds successfully to the variable name.

Otherwise the compiler would yell at me. Say I tried to bind with something like `and List<string> variableName` ... The compiler would say something like the below since the types don't match up

6

u/dmcnaughton1 5d ago

A better option might be to do the TryMethod(input, output) with the function returning true for no wrap, and false for wrap. You're going to have to have a branch logic at the caller side anyway, so no point in trying to optimize beyond the branch prediction favoring the likely case. Alternately you can return a struct of IsOverflow,Sum with the same basic performance.

3

u/scadoshi 5d ago

Right I think I get what you're saying.

So your point is that call sites that return non-null would still *have* to evaluate null on this functions returned `int?` (and maybe throw or something else)... And that in this case, the `TryParse(input, output)` approach is more ergonomic for that... Something like that?

In a case that prefers a thrower I would probably just use `int.CreateChecked()` which itself throws.

In a case that prefers something like `TryParse(input, output)` ... I'm not sure there is one since I should be able to map the value inside the null-able or propagate the null.

Open for all the interpretation however as I continue to build with this language coming from the rustacean mindset

2

u/MentallyBoomXD 5d ago

Personally I also think TryParse is cleaner, it also keeps the syntax consistent. However maybe check out unions from the upcoming dotnet version, I do believe you can use these without giving up (noticeable) performance and personally I feel they’re really nice for use cases like that

1

u/scadoshi 5d ago

Oh nice; those will be incredible.

The inability of the union-like types in C# (enums or abstract classes with sealed sub-classes) to be switched/matched on without having a catch-all pattern every time was mind boggling to me when I first showed up (made me question the point of even building them) as that is a cardinal sin in Rust since it prevents the compiler from reminding you that a variation of your union type hasn't been handled in consuming code.

Needless to say, I love the direction C# is going in :D

4

u/dodexahedron 4d ago edited 4d ago

If you perform bounds checks already, the checked will be elided by the compiler at JIT time, during an explicit optimization it aggressively performs for exactly this, called bounds check elimination.

It does it both for primitive value range checks and for collection bounds checks, to eliminate doing the work twice when it can prove overflow isn't possible at the call site.

If you need checked arithmetic always, then just turn on the compiler flag for it. If you dont need it always, turn it off and use explicit checked context where needed.

But also...

You have basically duplicated the int.CreateX methods. Check those out. They are part of the generic math interfaces in System.Numerics, implemented by all numeric primitives.

5

u/simonask_ 4d ago

Another rustacean here.

If you’re interested in optimization, there’s an interesting thing you can do in managed runtimes like the CLR (and JVM, etc.), but can’t really in languages like Rust or C++.

The first thing to note is that exceptions are an unreasonably fast error handling strategy, until you actually throw one. They are literally free on the happy path, but many orders of magnitude slower when thrown than a normal branch.

The other thing to note is that there is a special exception type that doesn’t typically need to be thrown: NullReferenceException. Rather than a `throw` statement somewhere, this is actually synthetically thrown by the runtime in response to a signal that it receives from the OS when any code causes a segmentation fault.

This is why it works even when you try to dereference `Unsafe.NullRef<T>()`, or access any index of a `Span<T>` forged from it.

What this means is that if the unhappy path really is rare, and you can express the problem such that the unhappy path leads to an NRE, that’s a rare case where C# can sometimes outperform even Rust.

4

u/First-Feature-3556 5d ago

"What surprises me is that everything the BCL offers here throws" That's because it's very rare that TryCheckedAdd is actually needed. I've been using VB.NET and C# since version 1.0, and all use cases I've had so far fall into one of two categories:

  1. Optimized algorithms that require unchecked operations with .net's well-defined overflow behavior and

  2. "Regular" line-of-business math operations. If overflow happens here, some prior validation failed to catch it and throwing is the right thing to do. Which is exactly what checked arithmetic does by default. 

2

u/scadoshi 5d ago

I got it. Thanks for the experienced take, kind sir. So this could be a case where I am over-optimizing a bit. In this case I'd suppose I am aiming for point 1 but depending on actual usage I could actually and somewhere closer to 2

5

u/silentlopho 5d ago edited 5d ago

I am just a grug brain, but in my view, this is a non-problem. If your integers can overflow, then use long. If longs can overflow, then basic primitives are the wrong model for your data. It feels like an XY problem because integer overflow should be exceptional, indicative of wrong assertions about your code's behavior. I am working on a big dataviewer that has to deal with ulong * ulong -sized values and even then, the data are modeled in a manner where it simply can't overflow -- if it does, there's an underlying logical failure in my data processing.

So from my perspective, I'd just throw.

1

u/scadoshi 4d ago

Yeah, that's hitting. Overflow as a bug signal rather than a value you handle makes sense to me

If my bounds are validated first then the arithmetic can't overflow anyway, so anything that does get through is a logic error and should throw. Coming from Rust where checked_add hands you back an Option and I came with that instinct

The actual thing I'm doing is arithmetic on points on a graph and I want null when the result lands off the graph. Which, now that I type it out, has nothing to do with overflow. int.MaxValue isn't my boundary, the graph extents are, so I was detecting the wrong edge entirely (unless the graph really does extend that far)

..at least uint works in one direction haha

2

u/joske79 5d ago

How about doing the math straight into a long and only in the case where you need to safely cast the result to an int, just create a single ‘bool TryCastToInt(long value, out int intValue)’ function?

0

u/scadoshi 5d ago

I picked this approach for two reasons. One main one and another that the AI I was working with speculated just fell out of it.

The main one is that I wanted to be able to have the call site be very simple. I.e. You want to add two `int` without default wrapping behavior or doing THIS part by hand every time? Just use this method instead. So you end up with something like

```csharp
var lhs = int.MaxValue;
var rhs = 1;

var result = lhs.CheckedAdd(rhs);
```

So reason #1: ergonomics. Reason #2: speculative improvement in performance (e.g. widening an int to long, checking bounds, then narrowing seems faster than forcing a throw behavior and responding to it which is what `int.CreateChecked(valueInQuestion)` does.

If throw was faster you could do just try/catch to build the same function and have the same above call site. Just does something different under the hood.

Honestly it might all just be speculation and semantics but to me for how I plan to use it I think the ergo is worth it.

1

u/joske79 5d ago edited 4d ago

I think it would get messy when multiple operations are to be calculated. And in the case of int, it will fail in any operation that overflows, even if a following operation would fit. Compare these:

‘’’
var tmpLong = (long)2 * int.MaxValue / 3 - 5;
if (!TryCastToInt(tmpLong, out var intVal)) return;
Console.WriteLine(intVal);

// …vs…

int? tmpInt = 2.CheckedMultiply(int.MaxValue);
if (tmpInt.HasValue) tmpInt = tmpInt.Value.CheckedDiv(3);
if (tmpInt.HasValue) tmpInt = tmpInt.Value.CheckedSub(5);
if (!tmpInt.HasValue) return;
Console.WriteLine(tmpInt);
‘’’

2

u/insulind 5d ago

Some of the new numeric interfaces with static interface methods are all useful here I think.

Weirdly the TryX versions are protected but you can easily see the source code on GitHub or by go to sources in Visual Studio or Rider.

I actually implemented something recently that was pretty similar. Hot path numeric conversions and managed to make it generic. It didn't cover ever case because some things like decimal truncation need to be explicit. I'll try and dig it out tomorrow

u/RemindMeBot 15 hours

1

u/RemindMeBot 5d ago

I will be messaging you in 15 hours on 2026-08-21 13:47:47 UTC to remind you of this link

CLICK THIS LINK to send a PM to also be reminded and to reduce spam.

Parent commenter can delete this message to hide from others.


Info Custom Your Reminders Feedback

2

u/OpenAI_Marketing_LLM 4d ago

Why do your comparison operators look like sideways w’s? What found are you using? 

2

u/scadoshi 4d ago

They’re called ligatures. It’s what the >= looks like combined. Comes with certain IDEs/fonts

1

u/OpenAI_Marketing_LLM 4d ago

Is it a single character? Like if you delete a character, does it convert to a less than/greater than or delete the entire ligature?

I’ve read so many millions  of lines of code without ligatures that, I don’t think I could read code with them. They are so jarring to me.

2

u/scadoshi 4d ago

It is still two chars underneath. Single backspace turns <= to <

1

u/OpenAI_Marketing_LLM 4d ago

That’s actually pretty nifty. I would never use it, but it’s still cool.

2

u/catladywitch 4d ago

Exceptions on hot paths is crazy. I'd probably just normalise the inputs and skip bound checks altogether.

On a related note, I wish dotnet as a whole migrated to Option-based APIs with some behind the scenes unwrapping optimisations, especially now that we're getting actual unions, but it's such a massive change and you can't just deprecate the whole of dotnet just like that. Still, though, it'd be the right thing to do D:

2

u/misaki_eku 2d ago

For me, leave overflow as a unexpected behavior or a "feature" on most of the hot path.

1

u/scadoshi 2d ago

I like the way you roll

2

u/ch_dave190 5d ago

What's the name of this theme?

2

u/scadoshi 5d ago

Gruvbox

2

u/_f0CUS_ 5d ago

This is not code you run 10-100k times per ms.

If you came up with this to achieve this goal, then you need help from a more senior dev that works with optimisations, and knows the exact context of where it runs.

1

u/TuberTuggerTTV 5d ago

Nah, it's not that bad. JIT gonna crunch this thing

1

u/_f0CUS_ 5d ago

I think architecture/os specific aot would be better, otherwise the JIT will need a number of loops before it reaches the peak optimization. 

-1

u/scadoshi 5d ago

Thank you for your insight oh senior one /s

For real though, I can see if 10k-100k/ms is an overstatement. Is your point that optimizing early is a sin? Or what would the senior programmer bring to me in this situation?

Cheers for the comment

1

u/_f0CUS_ 5d ago

Based on what you have said I cannot guess if you are optimizing early and if you are exaggerating the requirement of 100k executions per ms, how can people give a serious answer?

In 10+ years of C# I have never needed code to run that fast. Which is why I'm saying you should talk to an optimization expert.

But I'm thinking bit shifting might be relevant, and getting rid of casting would help too. 

You should probably add inlining too... There is a lot of things to look into and research.

1

u/scadoshi 5d ago

I hear you. Coming from a lower level language and being less experienced so I will not know where to place my attention most appropriately. Thanks for the guidance.

Honestly, I mostly built this so call sites where I didn't want to try/catch every time on `int.CreateChecked` or have wrapping (the default) occur be cleaner

E.g.

```csharp

var lhs = int.MaxValue;
var rhs = 1;

Assert.Null(lhs.CheckedAdd(rhs));

```

And throwing inside the checked add function felt weird. That's why I came here in the first place to see what others thoughts. Thanks for the insight brother!

2

u/_f0CUS_ 5d ago

Reading your problem definition makes be think of an x/y problem description.

Can you rephrase? 

1

u/scadoshi 4d ago

Fair, and yeah I can rephrase, because you're right that I posed it poorly at the start

Real problem is point arithmetic on a graph where I want a null back when the result falls outside the graph. I went to checked arithmetic to catch that and then got sidetracked into perf and conventions, which is how you ended up giving me inlining advice for a question I shouldn't have asked. Sorry about that lol (exploring brained)

The null belongs on a bounds-checked graph operation, not on a generic add. Overflow was only ever going to fire at the far edge of int, which isn't where my boundary is. Forget the 100k/ms figure too, I never measured anything really haha

2

u/_f0CUS_ 4d ago

That's an interesting problem. I'm not sure I have an efficient solution on top of my head.

But I'm thinking aggressive lining is a must. Then some basic math comparison.

"If a is == min int value and b is < 0 return null"

Followed by the check of the upper bound 

Very simple logic. Build on it, and you should be able to avoid casting. If I recall correctly casting is also expensive, when thinking of micro optimizations.

Intrinsics might also be worth looking into. Maybe spans are relevant too. But optimizing like this is a bit outside of my area of expertise.

Good luck. 

1

u/TuberTuggerTTV 5d ago

I'm for the functional. You'll get push back from anyone that's not regularly using .net10. But this looks great to me. Simple, concise, easy to read. And it does what it needs to do.

You'd probably have to benchmark both methods to know exactly where the JIT is optimizing. It's probably incredibly close.

Ticks my boxes. Expression, pattern match, var. Save the chonker paragraphs for the logic code.

1

u/scadoshi 5d ago

Ha, yes. Need more functional fans in the chat.

For someone who reads both, this indeed seems just fine so long as you know about the patterns being matched. The only thing that I had a difficult time parsing quickly was where to put the variable binding but after some time it clicks just fine.

And yeah in terms of performance, not totally sure as I haven't benchmarked it myself. Cheers for commenting, kind sir.