To answer the title question about how compilers handle if(false)
:
They optimize away constant branch conditions (and the dead code)
The language standard does not of course require compilers to not be terrible, but the C++ implementations that people actually use are non-terrible in this way. (So are most C implementations, except for maybe very simplistic non-optimizing ones like tinycc.)
One of the major reasons C++ is designed around if(something)
instead of the C preprocessor's #ifdef SOMETHING
is that they're equally efficient. Many C++ features (like constexpr
) only got added after compilers already implemented the necessary optimizations (inlining + constant propagation). (The reason we put up with all the undefined-behaviour pitfalls and gotchas of C and C++ is performance, especially with modern compilers that aggressively optimize on the assumption of no UB. The language design typically doesn't impose unnecessary performance costs.)
But if you care about debug-mode performance, the choice can be relevant depending on your compiler. (e.g. for a game or other program with real-time requirements for a debug build to even be testable).
e.g. clang++ -O0
("debug mode") still evaluates an if(constexpr_function())
at compile time and treats it like if(false)
or if(true)
. Some other compilers only eval at compile-time if they're forced to (by template-matching).
There is no performance cost for if(false)
with optimization enabled. (Barring missed-optimization bugs, which might depend on how early in the compile process the condition can be resolved to false and dead-code elimination can remove it before the compiler "thinks about" reserving stack space for its variables, or that the function may be non-leaf, or whatever.)
Any non-terrible compiler can optimize away dead code behind a compile-time-constant condition (Wikipedia: Dead Code Elimination). This is part of the baseline expectations people have for a C++ implementation to be usable in the real world; it's one of the most basic optimizations and all compilers in real use do it for simple cases like a constexpr
.
Often constant-propagation (especially after inlining) will make conditions compile-time constants even if they weren't obviously so in the source. One of the more-obvious cases is optimizing away the compare on the first iterations of a for (int i=0 ; i<n ; i++)
so it can turn into a normal asm loop with a conditional branch at the bottom (like a do{}while
loop in C++) if n
is constant or provably > 0
. (Yes, real compilers do value-range optimizations, not just constant propagation.)
Some compilers, like gcc and clang, remove dead code inside an if(false)
even in "debug" mode, at the minimum level of optimization that's required for them to transform the program logic through their internal arch-neutral representations and eventually emit asm. (But debug mode disables any kind of constant-propagation for variables that aren't declared const
or constexpr
in the source.)
Some compilers only do it when optimization is enabled; for example MSVC really likes to be literal in its translation of C++ to asm in debug mode and will actually create a zero in a register and branch on it being zero or not for if(false)
.
For gcc debug mode (-O0
), constexpr
functions aren't inlined if they don't have to be. (In some places the language requires a constant, like an array size inside a struct. GNU C++ supports C99 VLAs, but does choose to inline a constexpr function instead of actually making a VLA in debug mode.)
But non-function constexpr
s do get evaluated at compile time, not stored in memory and tested.
But just to reiterate, at any level of optimization, constexpr
functions are fully inlined and optimized away, and then the if()
Examples (from the Godbolt compiler explorer)
#include <type_traits>
void baz() {
if (std::is_integral<float>::value) f1(); // optimizes for gcc
else f2();
}
All compilers with -O2
optimization enabled (for x86-64):
baz():
jmp f2() # optimized tailcall
Debug-mode code quality, normally not relevant
GCC with optimization disabled still evaluates the expression and does dead-code elimination:
baz():
push rbp
mov rbp, rsp # -fno-omit-frame-pointer is the default at -O0
call f2() # still an unconditional call, no runtime branching
nop
pop rbp
ret
To see gcc not inline something with optimization disabled
static constexpr bool always_false() { return sizeof(char)==2*sizeof(int); }
void baz() {
if (always_false()) f1();
else f2();
}
static constexpr bool always_false() { return sizeof(char)==2*sizeof(int); }
void baz() {
if (always_false()) f1();
else f2();
}
;; gcc9.1 with no optimization chooses not to inline the constexpr function
baz():
push rbp
mov rbp, rsp
call always_false()
test al, al # the bool return value
je .L9
call f1()
jmp .L11
.L9:
call f2()
.L11:
nop
pop rbp
ret
MSVC's braindead literal code-gen with optimization disabled:
void foo() {
if (false) f1();
else f2();
}
;; MSVC 19.20 x86-64 no optimization
void foo(void) PROC ; foo
sub rsp, 40 ; 00000028H
xor eax, eax ; EAX=0
test eax, eax ; set flags from EAX (which were already set by xor)
je SHORT $LN2@foo ; jump if ZF is set, i.e. if EAX==0
call void f1(void) ; f1
jmp SHORT $LN3@foo
$LN2@foo:
call void f2(void) ; f2
$LN3@foo:
add rsp, 40 ; 00000028H
ret 0
Benchmarking with optimization disabled is not useful
You should always enable optimization for real code; the only time debug-mode performance matters is when that's a pre-condition for debugability. It's not a useful proxy to avoid having your benchmark optimize away; different code gains more or less from debug mode depending on how it's written.
Unless that's a really big deal for your project, and you just can't find enough info about local vars or something with minimal optimization like g++ -Og
, the headline of this answer is the full answer. Ignore debug mode, only bother thinking about quality of the asm in optimized builds. (Preferably with LTO enabled, if your project can enable that to allow cross-file inlining.)