简体   繁体   中英

Does noexcept improve performance?

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?

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.

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.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM