Noexcept, Stack Unwinding and Performance

noexcept, stack unwinding and performance

There's "no" overhead and then there's no overhead. You can think of the compiler in different ways:

  • It generates a program which performs certain actions.
  • It generates a program satisfying certain constraints.

The TR says there's no overhead in the table-driven appraoch because no action needs to be taken as long as a throw doesn't occur. The non-exceptional execution path goes straight forward.

However, to make the tables work, the non-exceptional code still needs additional constraints. Each object needs to be fully initialized before any exception could lead to its destruction, limiting the reordering of instructions (e.g. from an inlined constructor) across potentially throwing calls. Likewise, an object must be completely destroyed before any possible subsequent exception.

Table-based unwinding only works with functions following the ABI calling conventions, with stack frames. Without the possibility of an exception, the compiler may have been free to ignore the ABI and omit the frame.

Space overhead, a.k.a. bloat, in the form of tables and separate exceptional code paths, might not affect execution time, but it can still affect time taken to download the program and load it into RAM.

It's all relative, but noexcept cuts the compiler some slack.

Why don't throw() and noexcept have any overhead?

gcc and clang have very similar code generation as they are largely ABI compatible with one another. I only have clang to answer your question with, but my answer should apply to your gcc compiler fairly closely.

One can disassemble throw_func, noexcept_func and std_func using the -S command line flag. Upon doing so, you will note that the three functions all generate remarkably similar assembly.

Differences include:

  1. The mangled name of the functions will differ: __Z10throw_funcv, __Z13noexcept_funcv, and __Z8std_funcv.

  2. The "normal path" assembly code will be identical for all three functions except perhaps for the names of labels.

  3. throw_func and noexcept_func will generate "exception tables" after the code. These tables instruct the low-level C++ runtime library how to unwind the stack. This includes instructions on what destructors must be run, what catch blocks to try, and in the case of these two functions, what to do if an exception tries to propagate out. throw_func will contain a call to ___cxa_call_unexpected and noexcept_func will contain a call to ___clang_call_terminate (for gcc that will be named something else).

  4. std_func will not contain calls to unexpected nor terminate. Nor will it have an "exception table". There are no destructors to run, no try/catch clauses to land in, and there is no need to call unexpected or terminate.

In summary, the only difference in these three functions is in the "exceptional path". And the "exceptional path" adds code size, but is never executed by your main. In real-world code, just the added code size can impact run-time performance. However for code that is executed often, and small enough to fit in cache (such as this test), the code size hit won't cause any run-time performance hit.

For completeness, here is the noexcept_func assembly. Everything below LBB0_1: is to handle the exceptional path. LBB0_1: thru Ltmp1: throws the exception. Ltmp2: is a "landing pad" that the runtime will branch to if an exception tries to unwind through here. And GCC_except_table0: is the exception table itself.

    .globl  __Z13noexcept_funcv
.align 4, 0x90
__Z13noexcept_funcv: ## @_Z13noexcept_funcv
.cfi_startproc
.cfi_personality 155, ___gxx_personality_v0
Leh_func_begin0:
.cfi_lsda 16, Lexception0
## BB#0:
pushq %rbp
Ltmp3:
.cfi_def_cfa_offset 16
Ltmp4:
.cfi_offset %rbp, -16
movq %rsp, %rbp
Ltmp5:
.cfi_def_cfa_register %rbp
movq _val(%rip), %rax
leaq 1(%rax), %rcx
movq %rcx, _val(%rip)
testq %rax, %rax
je LBB0_1
LBB0_2:
popq %rbp
retq
LBB0_1:
movl $1, %edi
callq ___cxa_allocate_exception
Ltmp0:
movq __ZTI9exception@GOTPCREL(%rip), %rsi
xorl %edx, %edx
movq %rax, %rdi
callq ___cxa_throw
Ltmp1:
jmp LBB0_2
LBB0_3:
Ltmp2:
movq %rax, %rdi
callq ___clang_call_terminate
.cfi_endproc
Leh_func_end0:
.section __TEXT,__gcc_except_tab
.align 2
GCC_except_table0:
Lexception0:
.byte 255 ## @LPStart Encoding = omit
.byte 155 ## @TType Encoding = indirect pcrel sdata4
.asciz "\242\200\200" ## @TType base offset
.byte 3 ## Call site Encoding = udata4
.byte 26 ## Call site table length
Lset0 = Leh_func_begin0-Leh_func_begin0 ## >> Call Site 1 <<
.long Lset0
Lset1 = Ltmp0-Leh_func_begin0 ## Call between Leh_func_begin0 and Ltmp0
.long Lset1
.long 0 ## has no landing pad
.byte 0 ## On action: cleanup
Lset2 = Ltmp0-Leh_func_begin0 ## >> Call Site 2 <<
.long Lset2
Lset3 = Ltmp1-Ltmp0 ## Call between Ltmp0 and Ltmp1
.long Lset3
Lset4 = Ltmp2-Leh_func_begin0 ## jumps to Ltmp2
.long Lset4
.byte 1 ## On action: 1
.byte 1 ## >> Action Record 1 <<
## Catch TypeInfo 1
.byte 0 ## No further actions
## >> Catch TypeInfos <<
.long 0 ## TypeInfo 1
.align 2

noexcept practice for style and performance?

If your inline function is a leaf-level function, i.e. It calls no functions itself, then in theory a compiler could determine that it won't throw and omit whatever exception handling may have been generated otherwise. So performance-wise, it may prove unnecessary.

Having said that, you shouldn't expect to see a performance reduction from adding noexcept. Whatever code that would have to have been generated to handle propagation of exceptions shouldn't become any more complicated by adding noexcept. It's worth noting that a compiler is permitted to omit unwinding the stack entirely if an exception is thrown from a noexcept function. This is largely where the direct benefits of noexcept come from.

As for a style recommendation, first and foremost, consider whether noexcept would be a useful part of your interface. Things such as move operations can benefit greatly from being noexcept for algorithmic reasons, but besides those, it's really up to you to decide where noexcept has value for you, your interface and the users of your interface.

If this doesn't answer your question, feel free to comment on my answer and I will clarify further.

Sidenote: throw(), as well being deprecated in C++11, doesn't give the same guarantees as noexcept. If an exception is thrown through a function declared throw(), the stack must be completely unwound up to the caller of that function. See 15.5.2.1 in version N3337 of the C++ standard for reference to this behaviour.

Does noexcept improve performance?

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).

Does noexcept specification of swap has performance impact?

In addition to code that specifically behaves differently when something is noexcept (such as std::vector), declaring a function noexcept can allow the compiler to do its own optimizations. At the very least the compiler doesn't have to keep track of certain things that are involved in exception handling, which might free up registers or execute less instructions among other things.

How does noexcept in C++ change the assembly?

Pretty simple examples can be constructed that involve destructors directly rather than introspection on noexcept status:

void a(int);
void b() noexcept;
void c(int i) {
struct A {
int i;
~A() {a(i);}
} a={i};
b();
a.i=1;
}

Here, the noexcept allows the initialization of a in the caller to be ignored, since the destructor cannot observe it.

struct B {~B();};
void f();
void g() noexcept {
B b1;
f();
B b2;
}

Here, the noexcept allows the omission of frame information needed in case the callee throws. This depends on the (very common) decision to not unwind the stack when calling std::terminate.

How to use noexcept in C++ or How does it work?

A noexcept specification on a function is merely a method for a programmer to inform the compiler whether or not a function should throw exceptions.

The compiler can use this information to enable certain optimizations on non-throwing functions as well as enable the noexcept operator, which can check at compile time if a particular expression is declared to throw any exceptions.

For example, containers such as std::vector will move their elements if the elements' move constructor is noexcept, and copy otherwise (unless the copy constructor is not accessible, but a potentially throwing move constructor is, in which case the strong exception guarantee is waived).

noexcept is an improved version of throw(), which is deprecated in C++11. Unlike throw(), noexcept will not call std::unexpected and may or may not unwind the stack, which potentially allows the compiler to implement noexcept without the runtime overhead of throw().

For more details, please visit below websites

  • https://akrzemi1.wordpress.com/2014/04/24/noexcept-what-for/
  • When should I really use noexcept?

Edit: Sample source code to illustrate above points.

// whether foo is declared noexcept depends on if the expression
// T() will throw any exceptions, check in compile time
template <class T>
void foo() noexcept(noexcept(T())) {
}

void bar() noexcept(true) {
}

void baz() noexcept {
throw 42;
} // noexcept is the same as noexcept(true)

int main()
{
foo<int>(); // noexcept(noexcept(int())) => noexcept(true), so this is fine

bar(); // fine
baz(); // compiles, but at runtime this calls std::terminate
}


Related Topics



Leave a reply



Submit