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?
That noexcept keyword is tricky, but just know that if you use it, your coding world will spin faster.
There are two good reasons for the use of noexcept: First, an exception specifier documents the behaviour of the function. If a function is specified as noexcept, it can be safely used in a non-throwing function. Second, it is an optimisation opportunity for the compiler.
When an exception is thrown from a function that is declared noexcept or noexcept(true) , std::terminate is invoked. When an exception is thrown from a function declared as throw() in /std:c++14 mode, the result is undefined behavior. No specific function is invoked.
The noexcept operator performs a compile-time check that returns true if an expression is declared to not throw any exceptions. It can be used within a function template's noexcept specifier to declare that the function will throw exceptions for some types but not others.
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:
noexcept
if
throw()
already,noexcept
annotation cannot be correctly deduced by compiler and their instances are supposed to be put into some STL container.noexcept
if
std::terminate
,noexcept
or not.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).
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With