Why does enabling undefined behaviour sanitization interfere with optimizations?

Undefined behavior sanitizers are not a compiler-time-only mechanism (emphasis not in the original; and the quote is about clang but it applies to GCC as well):

UndefinedBehaviorSanitizer (UBSan) is a fast undefined behavior detector. UBSan modifies the program at compile-time to catch various kinds of undefined behavior during program execution.

So, instead of the original program - what actually gets compiled is a program with some additional "instrumentation" that you can see in the longer compiled code, e.g.:

  • Additional instructions which the original program would should not be able to get to.
  • An indication of where in the standard-library code the inappropriately-executed code is related.

Apparently, GCC's optimizers can't detect that there actually won't be any undefined behavior, and drop the unused code.


Especially since the code doesn't seem to have any UB hazards

f() returns a std::string_view which contains a length and a pointer. The call to x.substr(1, 3) requires adding one to that pointer. That technically may overflow. That is the potential UB. Change 1 to 0 and see the UB code go away.

We know that [ptr, ptr+5] are valid, so the conclusion is that gcc fails to propagate that knowledge of the value range, despite aggressive inlining and other simplification.

I can't find a directly related gcc bug, but this comment seems interesting:

[VRP] does an incredibly bad job at tracking pointer ranges where it simply prefers to track non-NULL.


Sanitizers add necessary instrumentation to detect violations at run-time. That instrumentation may prevent the function from being computed at compile-time as an optimization by introducing some opaque calls/side-effects that wouldn't be present there otherwise.

The inconsistent behavior you see is because g().length(); call is not done in constexpr context, so it's not required (well, "not expected" would be more accurate) to be computed at compile-time. GCC likely has some heuristics to compute constexpr functions with constexpr arguments in regular contexts that don't trigger once sanitizers get involved by either breaking the constexpr-ness of the function (due to added instrumentation) or one of the heuristics involved.

Adding constexpr to x makes f() call a constant expression (even if g() is not), so it's compiled at compile-time so it doesn't need to be instrumented, which is enough for other optimizations to trigger.

One can view that as a QoI issue, but in general it makes sense as

  1. constexpr function evaluation can take arbitrarily long, so it's not always preferable to evaluate everything at compile time unless asked to
  2. you can always "force" such evaluation (although the standard is somewhat permissive in this case), by using such functions in constant expressions. That'd also take care of any UB for you.