为什么启用未定义的行为清理会干扰优化?
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 危害...
备注:
- 我怀疑是 GCC 错误,但也许我对 UBsan 的功能有错误的认识。
- 如果我设置
-O3
,行为相同。
- 在没有优化标志的情况下,无论是否经过清理都会生成较长的代码。
- 如果您将
x
声明为 constexpr
变量,清理不会阻止优化。
- 与 C++17 和 C++20 的行为相同。
- 使用 Clang,您也 get 这种差异,但只有更高的优化设置(例如
-O3
)。
消毒剂添加了必要的仪器来检测 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 问题,但总的来说这是有道理的
constexpr
函数求值可以花费任意长的时间,因此除非被要求 ,否则在编译时求值并不总是可取的
- 您始终可以通过在常量表达式中使用此类函数来“强制”此类评估(尽管在这种情况下标准有些宽松)。这也会为您处理任何 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.
考虑以下代码:
#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 危害...
备注:
- 我怀疑是 GCC 错误,但也许我对 UBsan 的功能有错误的认识。
- 如果我设置
-O3
,行为相同。 - 在没有优化标志的情况下,无论是否经过清理都会生成较长的代码。
- 如果您将
x
声明为constexpr
变量,清理不会阻止优化。 - 与 C++17 和 C++20 的行为相同。
- 使用 Clang,您也 get 这种差异,但只有更高的优化设置(例如
-O3
)。
消毒剂添加了必要的仪器来检测 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 问题,但总的来说这是有道理的
constexpr
函数求值可以花费任意长的时间,因此除非被要求 ,否则在编译时求值并不总是可取的
- 您始终可以通过在常量表达式中使用此类函数来“强制”此类评估(尽管在这种情况下标准有些宽松)。这也会为您处理任何 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.