c++gcccompiler-optimizationconstexprubsan

Why does enabling undefined behaviour sanitization interfere with optimizations?


Consider the following code:

#include <string_view>

constexpr std::string_view f() { return "hello"; }

static constexpr std::string_view g() {
    auto x = f();
    return x.substr(1, 3);
}

int foo() { return g().length(); }

If I compile it with GCC 10.2, and flags --std=c++17 -O1, I get:

foo():
        mov     eax, 3
        ret

also, to my knowledge, this code does not suffer from any undefined behavior issues.

However - if I add the flag -fsanitize=undefined, the compilation result is:

.LC0:
        .string "hello"
foo():
        sub     rsp, 104
        mov     QWORD PTR [rsp+80], 5
        mov     QWORD PTR [rsp+16], 5
        mov     QWORD PTR [rsp+24], OFFSET FLAT:.LC0
        mov     QWORD PTR [rsp+8], 3
        mov     QWORD PTR [rsp+72], 4
        mov     eax, OFFSET FLAT:.LC0
        cmp     rax, -1
        jnb     .L4
.L2:
        mov     eax, 3
        add     rsp, 104
        ret
.L4:
        mov     edx, OFFSET FLAT:.LC0+1
        mov     rsi, rax
        mov     edi, OFFSET FLAT:.Lubsan_data154
        call    __ubsan_handle_pointer_overflow
        jmp     .L2
.LC1:
        .string "/opt/compiler-explorer/gcc-10.2.0/include/c++/10.2.0/string_view"
.Lubsan_data154:
        .quad   .LC1
        .long   287
        .long   49

See this on Compiler Explorer.

My question: Why should the sanitization interfere with the optimization? Especially since the code doesn't seem to have any UB hazards...

Notes:


Solution

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