为什么启用未定义的行为清理会干扰优化?

Why does enabling undefined behaviour sanitization interfere with optimizations?

考虑以下代码:

#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(); }

如果我用 GCC 10.2 编译它,并标记 --std=c++17 -O1,我得到:

foo():
        mov     eax, 3
        ret

此外,据我所知,这段代码没有任何未定义的行为问题。

但是-如果我添加标志-fsanitize=undefined,编译结果是:

.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

Compiler Explorer 上看到这个。

我的问题:为什么清理会干扰优化?特别是因为代码似乎没有任何 UB 危害...

备注:

消毒剂添加了必要的仪器来检测 run-time 处的违规行为。 该仪器可能会通过引入一些不透明的 calls/side-effects 来防止函数在 compile-time 处被计算为优化,否则不会出现在 calls/side-effects 处。

您看到的不一致行为是因为 g().length(); 调用不是在 constexpr 上下文中完成的,因此不需要(好吧,“不期望”会更准确)在 compile-time。 GCC 可能有一些启发式方法可以在常规上下文中使用 constexpr 参数计算 constexpr 函数,一旦通过破坏函数的 constexpr-ness(由于添加仪器)或涉及的启发式方法之一。

constexpr添加到x使得f()调用常量表达式(即使g()不是),所以它在compile-time处编译所以它不需要检测,这足以触发其他优化。

人们可以将其视为 QoI 问题,但总的来说这是有道理的

  1. constexpr 函数求值可以花费任意长的时间,因此除非被要求
  2. ,否则在编译时求值并不总是可取的
  3. 您始终可以通过在常量表达式中使用此类函数来“强制”此类评估(尽管在这种情况下标准有些宽松)。这也会为您处理任何 UB。

未定义的行为消毒剂是not一种compiler-time-only机制(强调不是原来的;引用是关于clang的,但它也适用于GCC):

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.

因此,实际编译的不是原始程序,而是带有一些额外“工具”的程序,您可以在较长的编译代码中看到这些程序,例如:

  • 原始程序无法访问的附加指令。
  • 指示 standard-library 代码中与 inappropriately-executed 代码相关的位置。

显然,GCC 的优化器无法检测到实际上不会有任何未定义的行为,并删除未使用的代码。

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

f() returns 包含长度和指针的 std::string_view。对 x.substr(1, 3) 的调用需要向该指针加一。从技术上讲,这可能会溢出。那就是潜在的UB。将 1 更改为 0,然后看到 UB 代码消失了。

我们知道 [ptr, ptr+5] 是有效的,所以结论是 gcc 无法传播值范围的知识,尽管进行了积极的内联和其他简化。

我找不到直接相关的 gcc 错误,但 this comment 似乎很有趣:

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