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

ein*_*ica 16 c++ gcc compiler-optimization constexpr ubsan

考虑以下代码:

#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(); }
Run Code Online (Sandbox Code Playgroud)

如果我用 GCC 10.2 和 flags 编译它--std=c++17 -O1,我会得到:

foo():
        mov     eax, 3
        ret
Run Code Online (Sandbox Code Playgroud)

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

但是 - 如果我添加 flag -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
Run Code Online (Sandbox Code Playgroud)

看到这个Compiler Explorer

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

笔记:

  • 我怀疑是 GCC 错误,但也许我对 UBsan 的作用有错误的看法。
  • 如果我设置了相同的行为-O3
  • 在没有优化标志的情况下,更长的代码会在经过清理和不经过清理的情况下生成。
  • 如果您声明xconstexpr变量,则清理不会阻止优化。
  • 与 C++17 和 C++20 的行为相同。
  • 使用 Clang,您也会得到这种差异,但只有使用更高的优化设置(例如-O3)。

Dan*_* M. 10

消毒剂添加了必要的工具来检测运行时的违规行为。该检测可能会通过引入一些不透明的调用/副作用来阻止函数在编译时作为优化计算,否则这些调用/副作用不会在那里出现。

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

添加constexprx使f()调用成为一个常量表达式(即使g()不是),因此它在编译时编译,因此不需要检测,这足以触发其他优化。

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

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


Jef*_*ett 5

特别是因为代码似乎没有任何 UB 危害

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

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

我找不到直接相关的 gcc 错误,但这条评论似乎很有趣:

[VRP] 在跟踪指针范围方面做得非常糟糕,它只是喜欢跟踪非 NULL。