August 5th, 2024

C++'s `Noexcept` Can (Sometimes) Help (Or Hurt) Performance

The article examines the mixed performance effects of the `noexcept` keyword in C++, revealing that its impact varies by compiler, OS, and code context, urging cautious application by developers.

Read original articleLink Icon
FrustrationSkepticismConfusion
C++'s `Noexcept` Can (Sometimes) Help (Or Hurt) Performance

The article discusses the performance implications of using the `noexcept` keyword in C++ programming, particularly in the context of the PSRayTracing (PSRT) project. The author reflects on initial beliefs that `noexcept` would universally enhance performance by eliminating exception handling overhead. However, after conducting extensive A/B testing, the results were mixed. While some configurations showed a slight performance increase (up to 1%), others experienced a decrease of around 2%. The author notes that the impact of `noexcept` is highly dependent on various factors, including the compiler, operating system, and specific code context. The article highlights that while `noexcept` can help optimize certain operations, such as enabling move semantics in the Standard Library, it can also complicate debugging and testing due to its restrictive nature. The author concludes that while `noexcept` has its benefits, its overall effect on performance is nuanced and not as significant as initially thought. The findings suggest that developers should be cautious when applying `noexcept` indiscriminately across their code.

- The `noexcept` keyword can provide performance benefits but is not universally effective.

- Performance impact varies significantly based on compiler, OS, and code context.

- Some configurations showed up to a 1% increase in performance, while others saw a decrease of around 2%.

- Using `noexcept` can complicate debugging and testing processes.

- Developers should apply `noexcept` judiciously rather than as a blanket solution.

Related

Optimizing JavaScript for Fun and for Profit

Optimizing JavaScript for Fun and for Profit

Optimizing JavaScript code for performance involves benchmarking, avoiding unnecessary work, string comparisons, and diverse object shapes. JavaScript engines optimize based on object shapes, impacting array/object methods and indirection. Creating objects with the same shape improves optimization, cautioning against slower functional programming methods. Costs of indirection like proxy objects and function calls affect performance. Code examples and benchmarks demonstrate optimization variances.

Spending too much time optimizing for loops

Spending too much time optimizing for loops

Researcher Octave Larose discussed optimizing Rust interpreters, focusing on improving performance for the SOM language. They highlighted enhancing loop efficiency through bytecode and primitives, addressing challenges like Rust limitations and complex designs. Despite performance gains, trade-offs between efficiency and code elegance persist.

C++ Design Patterns for Low-Latency Applications

C++ Design Patterns for Low-Latency Applications

The article delves into C++ design patterns for low-latency applications, emphasizing optimizations for high-frequency trading. Techniques include cache prewarming, constexpr usage, loop unrolling, and hotpath/coldpath separation. It also covers comparisons, datatypes, lock-free programming, and memory access optimizations. Importance of code optimization is underscored.

Beyond Clean Code

Beyond Clean Code

The article explores software optimization and "clean code," emphasizing readability versus performance. It critiques the belief that clean code equals bad code, highlighting the balance needed in software development.

Clang vs. Clang

Clang vs. Clang

The blog post critiques compiler optimizations in Clang, arguing they often introduce bugs and security vulnerabilities, diminish performance gains, and create timing channels, urging a reevaluation of current practices.

AI: What people are saying
The discussion around the `noexcept` keyword in C++ reveals a variety of perspectives on its performance implications and usage.
  • Many commenters emphasize that `noexcept` can introduce performance costs, particularly when functions that can throw are incorrectly marked as `noexcept`.
  • There is a consensus that the benefits of `noexcept` are context-dependent, with specific scenarios like move constructors showing potential performance improvements.
  • Several users express skepticism about the article's conclusions, suggesting that the benchmarks may not accurately reflect the performance impact of `noexcept`.
  • Some commenters advocate for better compiler diagnostics to prevent misuse of `noexcept`, particularly in cases where functions can throw.
  • There is a call for deeper understanding and analysis of how `noexcept` interacts with compiler optimizations, with suggestions to use tools like Godbolt for examining generated code.
Link Icon 16 comments
By @terrymah - 5 months
Oh man, don't get me started. This was a point in a talk I gave years ago called "Please Please Help the Compiler" (what I thought was a clever cut at the conventional wisdom at the time of "Don't Try to Help the Compiler")

I work on MSVC backend. I argued pretty strenuously at the time that noexcept was costly and being marketed incorrectly. Perhaps the costs are worth it, but none the less there is a cost

The reason is simple: there is a guarantee here that noexcept functions don't throw. std::terminate has to be called. That has to be implemented. There is some cost to that - conceptually every noexcept function (or worse, every call to a noexcept function) is surrounded by a giant try/catch(...) block.

Yes there are optimizations here. But it's still not free

Less obvious; how does inlining work? What happens if you inline a noexcept function into a function that allows exceptions? Do we now have "regions" of noexceptness inside that function (answer: yes). How do you implement that? Again, this is implementable, but this is even harder than the whole function case, and a naive/early implementation might prohibit inlining across degrees of noexcept-ness to be correct/as-if. And guess what, this is what early versions of MSVC did, and this was our biggest problem: a problem which grew release after release as noexcept permeated the standard library.

Anyway. My point is, we need more backend compiler engineers on WG21 and not just front end, library, and language lawyer guys.

I argued then that if instead noexcept violations were undefined, we could ignore all this, and instead just treat it as the pure optimization it was being marketed as (ie, help prove a region can't throw, so we can elide entire try/catch blocks etc). The reaction to my suggestion was not positive.

By @plorkyeran - 5 months
The most common place where noexcept improves performance is on move constructors and move assignments when moving is cheaper than copying. If your type is not nothrow moveable std::vector will copy it instead of moving when resizing, as the move constructor throwing would leave the vector in an invalid state (while the copy constructor throwing leaves the vector unchanged).

Platforms with setjmp-longjmp based exceptions benefit greatly from noexcept as there’s setup code required before calling functions which may throw. Those platforms are now mostly gone, though. Modern “zero cost” exceptions don’t execute a single instruction related to exception handling if no exceptions are thrown (hence the name), so there just isn’t much room for noexcept to be useful to the optimizer.

Outside of those two scenarios there isn’t any reason to expect noexcept to improve performance.

By @Arech - 5 months
That's quite interesting and a huge work has been done here, respect for that.

Here's what has jumped out at me: `noexcept` qualifier is not free in some cases, particularly, when a qualified function could actually throw, but is marked `noexcept`. In that case, a compiler still must set something up to fulfil the main `noexcept` promise - call `std::terminate()` if an exception is thrown. That means, that putting `noexcept` on each and every function blindly without any regard to whether the function could really throw or not (for example, `std::vector::push_back()` could throw on reallocation failure, hence if a `noexcept` qualified function call it, a compiler must take into account) doesn't actually test/benchmark/prove anything, since as the author correctly said, - you won't ever do this in a real production project. It would be really interesting to take a look into a full code of cases that showed very bad performance, however, here we're approaching the second issue: if that's the core benchmark code: https://github.com/define-private-public/PSRayTracing/blob/a... then unfortunately it's totally invalid since it measures time with the `std::chrono::system_clock` which isn't monotonic. Given how long the code required to run, it's almost certain that the clock has been adjusted several times...

By @TillE - 5 months
> I didn't know std::uniform_int_distribution doesn't actually produce the same results on different compilers

I think this is genuinely my biggest complaint about the C++ standard library. There are countless scenarios where you want deterministic random numbers (for testing if nothing else), so std's distributions are unusable. Fortunately you can just plug in Boost's implementation.

By @hoten - 5 months
I don't feel like this article illuminates anything about how noexcept works. The asm diff at the end suggests _there is no difference_ in the emitted code. I plugged it into godbolt myself and see absolutely no difference. https://godbolt.org/z/jdro5jdnG

It seems the selected example function may not be exercising noexcept. I suppose the assumption is that operator[] is something that can throw, but ... perhaps the machinery lives outside the function (so should really examine function calls), or is never emitted without a try/catch, or operator[] (though not marked noexcept...) doesn't throw b/c OOB is undefined behavior, or ... ?

By @olliej - 5 months
I would like to have seen a comparison that actually includes -fno-exceptions, rather than just noexcept. My assumption is that to get a consistent gain from noexcept, you would need every function called to be explicitly noexcept, because a bunch of the cost of exceptions is code size and state required to support unwinding. So if the performance cost exception handling is causing is due to that, then if _anything_ can cause an exception (or I guess more accurately unless every opaque call is explicitly indicated to not cause an exception) then that overhead remains.

That said, I'm still confused by the perf results of the article, especially the perlin noise vs MSVC one. It's sufficiently weird outlier that it makes me wonder if something in the compiler has a noexcept path that adds checks that aren't usually on (i.e imagine the code has a "debug" mode that did bounds checks or something, but the function resolution you hit in the noexcept path always does the bounds check - I'm really not sure exactly how you'd get that to happen, but "non-default path was not benchmarked" is not exactly an uncommon occurrence)

By @compiler-guy - 5 months
Even a speedup of around 1% (if it is consistent and in a carefully controlled experiment) is significant for many workloads, if the workload is big enough.

The OP has this as in the fuzz, which it may be for that particular workload. But across a giant distributed system like youtube or Google search, it is a real gain.

By @muth02446 - 5 months
RE: unexpected performance degradation

programs can be quite sensitive to how code is laid out because of cache line alignment, cache conflicts etc.

So random changes can have a surprising impact.

There was a paper a couple of years ago explaining this and how to measure compiler optimizations more reliably. Sadly, I do not recall the title/author.

By @shultays - 5 months
I can't find the explanation on why noexcept could hurt performance. One reason I can see itt is some containers like unordered_map can inline the hash along with the key with noexcept, which may not worth additional memory overhead if the hashing is relatively cheap. He talks a bit about it in "Intel+Windows+MSVC" but not much info. I wish there was

noexcept helps in some cases that author doesn't seem to be using and any performance gain or loss is basically due to some (unrelated?) optimization decisions the compiler takes differently in noexcept builds if I am understanding correctly?

By @maccard - 5 months
This is super unrelated to the optimisation, and is just related to the cmake setup - instead of common.hpp having #ifdef USE_NOEXCEPT #define NOEXCEPT noexcept #else #define NOEXCEPT #endif

and cmake being:

    if (WITH_NOEXCEPT)
      message(STATUS "Using `noexcept` annotations (faster?)")
      target_compile_definitions(PSRayTracing_StaticLibrary PUBLIC USE_NOEXCEPT)
    else()
      message(STATUS "Turned off use of `noexcept` (slower?)")
    endif()
, the cmake could just be:

    if (WITH_NOEXCEPT)
      message(STATUS "Using `noexcept` annotations (faster?)")
      target_compile_definitions(PSRayTracing_StaticLibrary PUBLIC USE_NOEXCEPT=noexcept)
    else()
      message(STATUS "Turned off use of `noexcept` (slower?)")      target_compile_definitions(PSRayTracing_StaticLibrary PUBLIC USE_NOEXCEPT=)
    endif()
No need for these shared "common" config headers.

Back on topic, this doesn't surprise me. There's this idea that C++ is fast, and that people who work with C++ are focused on optimisation and in my experience there's as many of these theoretical ideas about performance which aren't backed up by numbers, but are now ingrained in people. See https://news.ycombinator.com/item?id=41095814 from last week for another example of dogmatic guidelines having the wrong impact.

By @quotemstr - 5 months
There's a lot of mysticism and superstition surrounding C++ exceptions. It's instructive to sit down with godbolt and examine specific scenarios in which noexcept (or exceptions generally) can affect performance. Read the machine code. Understand why the compiler does what it does. Don't want to invest at that level? You probably want to use a higher level language.
By @Night_Thastus - 5 months
I thought I saw this post, or a very similar one, a couple years ago. Does anyone else remember that? Yet I don't see it in the post history.
By @rwmj - 5 months
Shouldn't the compiler deduce noexcept for you?
By @hoseja - 5 months
Apologize less for using completely benign standard macros. They are an okay tool if not abused.
By @tolmasky - 5 months
Is there any compiler option to have it yell at you if you mark something that can throw as `noexcept`, which seems to be the cause of (at least some of) the slowdowns where the compiler is forced to accommodate with `std::terminate`? I feel like these situations are more commonly mistakes, and not the user wanting to "collapse" exceptions into terminations. So the current approach to dealing with these cases seems to be suboptimal not only from a performance perspective, but a behavior perspective as well.
By @Squeeeez - 5 months
Is this some kind of new clickbait title? Something "can" "sometimes" do something (already 0 information) - ooor sometimes it also does the opposite. The only possibility not allowed is that it would not make any difference, but this one is actually also possible. Sigh.