40

Is the noexcept function specifier aimed to improving the performance because of potentially no book-keeping code for exceptions in the generated object, and therefore should be added to function declarations and definitions whenever possible? I think of wrappers for callable objects in the first place, where noexcept could make some difference, although the checking expressions might "bloat" the source code. Is it worth?

Martin
  • 9,089
  • 11
  • 52
  • 87
  • 2
    I think a compiler could do that, but I think the point is to allow templates to behave differently when they can't throw an exception (there is a `noexcept` operator that returns `false` when the argument expression can return an exception- it doesn't return true if you have a function that will never throw but isn't tagged with `noexcept` though). – Cubic Apr 19 '13 at 12:08
  • If a function that is declared as `noexcept` throws the program must terminate. The compiler needs to inject at least some exception handling code to catch the issue and terminate the program. – David Rodríguez - dribeas Apr 19 '13 at 12:55
  • @DavidRodríguez-dribeas: that code can be out-of-line, though (part of the C++ runtime, even). In principle the exception-handling mechanism could walk up the stack and for each call frame, check whether its code is tagged with a "noexcept" flag. Perhaps with extra complication to deal with inlined calls that don't have their own frame. Whether that's what compilers actually do for `noexcept`, I'm not so sure, but often it is how they look for exception handlers and also for work to be done during stack unwinding. – Steve Jessop Apr 19 '13 at 12:58
  • 1
    @SteveJessop: I am not very familiar with the exception handling mechanism either, I just wanted to point out that it is not such a clear cut. Something still needs to be done, and the fact that a function is tagged `noexcept` does not mean that there cannot be exceptions inside either, only that they don't propagate through the function call (they can be thrown and caught inside the function). While there could be some room for optimizations I would not sprinkle `noexcept` all over the place for *performance*, as it can have unwanted side effects ... – David Rodríguez - dribeas Apr 19 '13 at 13:01
  • ... for example consider N3604, discussed just yesterday in the standard committee. When validating preconditions in narrow contracts a violation of the contract (i.e. undefined behavior) could be detected and one of multiple options chosen to help debugging. One of those options could be throwing an exception, but if the contract violation happens inside a call to a function that is `noexcept` that will transform into `std::terminate`. That is the reason that in Madrid they dropped `noexcept` from the signature of functions like `std::vector<>::operator[]` that is guaranteed not to throw! – David Rodríguez - dribeas Apr 19 '13 at 13:05
  • @DavidRodríguez-dribeas: Interesting. Once you have undefined behavior in `operator[]` due to an out of bounds index, I suppose you *could* have an exception that propagates past `noexcept`. But it might not be very useful or very easy to implement. I'd rather people used `at()` when they want an exception, and debugging implementations of `operator[]` halt in the debugger, or failing that terminate with a message. But that's just me, standard library implementers think otherwise... – Steve Jessop Apr 19 '13 at 13:37
  • @SteveJessop: Yes, although it would require *detecting* that you are already in undefined behavior. Basically the implementation of `operator[]` would have to do `if (pos<0 || pos>=size()) { enable_throw_throught_nothrow(); throw X(); } else return *(begin()+pos)`. That is, while the call of `v[-1]` is undefined behavior on the component, before you try to dereference the invalid memory you have not really caused *any* hard undefined behavior (not sure if soft/hard is discussed in that paper, but understand 'hard' undefined behavior as UB in the language, and 'soft' as only in the component) – David Rodríguez - dribeas Apr 19 '13 at 13:42
  • @DavidRodríguez-dribeas With regard to your first comment, terminating a program in a function declared noexcept does not necessarily mean that some exception handling must be generated in the code. From an observable point of view, std::terminate could be "called" directly instead of really throwing. Therefore I think at least in these cases noexcept might result in a effective improvement. – Martin Apr 19 '13 at 13:44
  • @Martin: Unless you change the exception model to be that of Java (in the stricter sense) you cannot do what you suggest. Say that in my design a function is supposed to test the limits of a vector and then call `std::vector<>::at()` with a valid number. I tag that function as `noexcept`. Then there is an issue in the implementation and `at()` is called with an index out of bounds. The implementation of `std::vector<>::at` does not know whether the caller is `nothrow` or not, it cannot decide to call `std::terminate` directly. And you don't want to track what exceptions will hit `nothrow`. – David Rodríguez - dribeas Apr 19 '13 at 13:51
  • @DavidRodríguez-dribeas I am not sure. Why do you think the compiler can not be smart enough to see that vector::at() (which can throw) is being called from a noexcept function, so that *the compiler* would be allowed to replace any throw with a std::terminate? – Martin Apr 19 '13 at 13:56
  • @Martin: If the function is not inline there can only be one definition, and that definition must throw (which is it's contract). The language allows calling functions that aren't tagged as `noexcept` or even functions tagged `noexcept(false)` from a `noexcept` function, it is up to the implementation of the function to determine that the particular combination of arguments won't cause the exception to be thrown. Finally, inside the `noexcept` function you can have a `try/catch` in which case it is perfectly fine to have the exception thrown [...] – David Rodríguez - dribeas Apr 19 '13 at 14:01
  • [...] but tracking what are the set of exceptions that will be caught by the possibly nested try/catch blocks and which ones would not and thus would have to call `std::terminate` is some work I don't expect compiler vendors to do. – David Rodríguez - dribeas Apr 19 '13 at 14:02

3 Answers3

23

Theoretically speaking, noexcept would improve performance. But it might also cause some problems on the other hand.

In most of cases, it shouldn't be specified because the pros are too few to be considered and it might make your code upgrading painful. This post, written by Andrzej, introduces the reasons in detail.

If it's too long, just take these suggestions I conclude from it:

  1. Annotate functions with noexcept if
    • they were annotated with throw() already,
    • or they are good candidates(listed in post) and never throw for sure,
    • or they are move-constructors, move-assignments whose noexcept annotation cannot be correctly deduced by compiler and their instances are supposed to be put into some STL container.
  2. Do not annotate the functions with noexcept if
    • you are really concerned about reduced performance,
    • or about the risk of calling std::terminate,
    • or you are just not sure about the new feature,
    • or you have doubts whether you should make your function noexcept or not.
Matt Yang
  • 661
  • 4
  • 8
16

I stumbled across a "real-world" example where noexcept makes a difference. I want to share it here because it might help others form an opinion.

First a little bit of background: Standard library containers try to be "exception safe". That means they give you certain guarantees on the state of a container after an exception has been raised (and caught). A very good example for this is std::vector::emplace_back. If the insertion fails for some reason, emplace_back guarantees that the vector appears to be unaltered. See the cppreference on emplace_back. This, however, gets interesting when the vector needs to relocate in response to the emplace. The (hopefully) fastest way to relocate the pre-existing vector items would be to move them to the new enlarged buffer. Unfortunately, move-construction could raise an exception, so if the value type's move-ctor is not exception safe, emplace_back needs to resort to the copy operation instead. But since it's possible to probe a type for its move-noexept'ness at compile time std::vector will still take the faster approach if that turns out to be legal.

I threw together the following google benchmark to measure this locally:

#include "benchmark/benchmark.h"

#include <vector>

// This type really benefits from being moved instead of being copied
struct SlowCopy {
  SlowCopy(const size_t theSize) {
    for (int i = 0; i < theSize; ++i)
      itsData.emplace_back(i);
  }
  SlowCopy(const SlowCopy &) = default;
  SlowCopy(SlowCopy &&) noexcept = default;

  std::vector<int> itsData;
};

// The template parameter specifies whether the move constructor is noexcept or not
template<bool YesNo>
struct MovableNoexcept {
  MovableNoexcept(const size_t theSize) : itsData{theSize} {}
  MovableNoexcept(const MovableNoexcept &) = default;
  MovableNoexcept(MovableNoexcept &&) noexcept(YesNo) = default;
  MovableNoexcept& operator=(const MovableNoexcept &)  = default;
  MovableNoexcept& operator=(MovableNoexcept &&) noexcept(false) = default;
  SlowCopy itsData;
};

// This benchmark takes 2 arguments:
// 1. How many items do we push into a vector
// 2. How big are the items that are in the vector
template<bool IsNoexcept>
static void BM_MoveRelocateNoexcept(benchmark::State& state) {
  std::vector<MovableNoexcept<IsNoexcept>> aExcepts;
  for (auto _ : state) {
    for (int i = 0; i < state.range(0); ++i)
      aExcepts.emplace_back(state.range(1));
    benchmark::ClobberMemory();
  }
}

// Test 1k elements @ 64*sizeof(int) kb
BENCHMARK_TEMPLATE(BM_MoveRelocateNoexcept, false)->Args({1000, 1 << 16})->Repetitions(20);
BENCHMARK_TEMPLATE(BM_MoveRelocateNoexcept, true)->Args({1000, 1 << 16})->Repetitions(20);

// Test 100 elements @ 512*sizeof(int) kb
BENCHMARK_TEMPLATE(BM_MoveRelocateNoexcept, false)->Args({100, 1 << 19})->Repetitions(20);
BENCHMARK_TEMPLATE(BM_MoveRelocateNoexcept, true)->Args({100, 1 << 19})->Repetitions(20);

// Run the benchmark
BENCHMARK_MAIN();

On my local system, I measured the following results running the benchmark:

Running ./noexcept_bench
Run on (8 X 4400 MHz CPU s)
CPU Caches:
  L1 Data 32 KiB (x4)
  L1 Instruction 32 KiB (x4)
  L2 Unified 256 KiB (x4)
  L3 Unified 8192 KiB (x1)
Load Average: 0.58, 0.70, 0.69
------------------------------------------------------------------------------------------------------
Benchmark                                                            Time             CPU   Iterations
------------------------------------------------------------------------------------------------------
BM_MoveRelocateNoexcept<false>/1000/65536/repeats:20_mean    157793886 ns    157556651 ns           20
BM_MoveRelocateNoexcept<false>/1000/65536/repeats:20_median  157752118 ns    157511285 ns           20
BM_MoveRelocateNoexcept<false>/1000/65536/repeats:20_stddev     294024 ns       292420 ns           20
BM_MoveRelocateNoexcept<true>/1000/65536/repeats:20_mean     119320642 ns    119235176 ns           20
BM_MoveRelocateNoexcept<true>/1000/65536/repeats:20_median   119256119 ns    119187012 ns           20
BM_MoveRelocateNoexcept<true>/1000/65536/repeats:20_stddev      190923 ns       180183 ns           20
BM_MoveRelocateNoexcept<false>/100/524288/repeats:20_mean    127031806 ns    126834505 ns           20
BM_MoveRelocateNoexcept<false>/100/524288/repeats:20_median  126939978 ns    126741072 ns           20
BM_MoveRelocateNoexcept<false>/100/524288/repeats:20_stddev     381682 ns       380187 ns           20
BM_MoveRelocateNoexcept<true>/100/524288/repeats:20_mean      95281309 ns     95175234 ns           20
BM_MoveRelocateNoexcept<true>/100/524288/repeats:20_median    95267762 ns     95152072 ns           20
BM_MoveRelocateNoexcept<true>/100/524288/repeats:20_stddev      176838 ns       176834 ns           20

Looking at those results, the tests where noexcept-move was possible saw a speedup of ~1.3 relative to their non-noexcept-movable counterparts in both benchmarks.

suluke
  • 683
  • 5
  • 14
  • According to this answer https://stackoverflow.com/a/46874444/889742 relocation still uses the throwing move if it is available. Do you have a source for the statement that if there is no non-throwing move it resorts to copy? – Gonen I Feb 22 '23 at 22:05
15

Top compilers produce code that is already optimized a lot like code that can't throw, and then the case when an exception occurs is handled by out-of-line code that the exception-handling mechanism finds by looking at meta-data concerning the function. I suppose there's some benefit in code size to omitting this when it's known not to be needed, though.

There are probably some cases where a nothrow specification does allow some specific optimization:

int main() {
    int i = 0;
    try {
        ++i;
        thing_that_cannot_throw();
        ++i;
        thing_that_can_throw();
        ++i;
    } catch (...) {}
    std::cout << i << "\n";
}

Here the second ++i could in theory be reordered before the call to thing_that_cannot_throw (and i just initialized to 2). Whether it is in practice is another matter, though, since an implementation that makes guarantees about the state of variables in the debugger or in the stack above a function call, would want i to have value 1 during that call even though it's a local variable not observable by any standard means.

I suspect that nothrow guarantees are more valuable to the programmer than to the compiler. If you're writing code that offers the strong exception guarantee then usually there will be certain critical operations you perform, that you need to offer the nothrow guarantee (swaps, moves and destructors being the common candidates).

Steve Jessop
  • 273,490
  • 39
  • 460
  • 699
  • And of course, we can have compilers *warning* about having exceptions possibly thrown from within `noexcept` functions to help us walk the line... though unfortunately with C++ the pervasive issue of `new` being able to throw prevents much :( – Matthieu M. Apr 19 '13 at 13:48
  • @MatthieuM. there is a nothrow version of new, isn't there? – Martin Apr 19 '13 at 13:49
  • 2
    @Martin: there is, what does `std::string` use though ? The trouble is that you cannot create a new `std::string` in a `noexcept` method; not because your code is flawed, but because the hardware you run on might be more limited. It's quite annoying. – Matthieu M. Apr 19 '13 at 13:56
  • 3
    @MatthieuM: Maybe I've misunderstood you, but of course you can create a string, you just have to handle the possibility of error. If you don't believe that an error can occur, because your hardware isn't so limited, then `void myfunc() noexcept { try { myfunc_impl(); } catch(...) {} }` would do it, although you might want write something a bit less supremely confident the op will succeed! Anyway `myfunc_impl` doesn't need to be a `noexcept` function. It's annoying only to the extent that writing exception-safe code is always annoying -- you can't ignore fallible operations. – Steve Jessop Apr 19 '13 at 14:28