从一个有趣的例子说起
一个很简单的 CPP 语言样本, 代码如下:
// main.cpp
#include <iostream>
int main(int argc, char* argv[]) {
while (1);
}
void myfunc() {
std::cout << "Hello World!";
}
使用如下命令编译代码并运行:
$ clang++ -O1 -g main.cpp -o main.elf
$ ./main.elf
Hello World!
显然, 这个代码时不符合我们对程序的理解的. 由于 main
函数中为一个死循环, 且没有发生任何的 myfunc
函数调用, 因此第 8 行的输出应该是不会被执行的. 但是运行结果却显示改代码被执行了.
这个原因是因为, 编译器在处理到 while(1);
语句时, 认为该语句及后续代码为一个 unreachable
语句, 此时生成的 IR 内容如下:
define dso_local noundef i32 @main(i32 noundef %0, i8** nocapture noundef readnone %1) local_unnamed_addr #3 !dbg !920 {
unreachable, !dbg !926
}
define dso_local void @_Z6myfuncv() local_unnamed_addr #4 !dbg !927 {
...
%1 = call noundef nonnull align 8 dereferenceable(8) %"class.std::basic_ostream"* @_ZSt16__ostream_insertIcSt11char_traitsIcEERSt13basic_ostreamIT_T0_ES6_PKS3_l(%"class.std::basic_ostream"* noundef nonnull align 8 dereferenceable(8) @_ZSt4cout, i8* noundef nonnull getelementptr inbounds ([13 x i8], [13 x i8]* @.str, i64 0, i64 0), i64 noundef 12), !dbg !997
...
}
显然, while(1);
语句在 IR 层面被优化为了 unreachable
语句. 尝试定位该语句是何时被优化的, 使用如下命令重新编译源代码, 并输出 LLVM Pass 每一次的优化结果:
# -O1 启用优化, 等级1
# -mllvm 将后续参数传递给 LLVM 工具链
# -print-after-all LLVM 工具链参数, 用于输出整个编译流程中每个 LLVM pass 的详细信息
# 2>&1 LLVM 工具链的输出全部采用 llvm::errs() 输出到 stderr 中, 需要重定向到 stdout 中
clang -O1 -g -Wall -mllvm -print-after-all main.cpp -o main.elf > opt_step.txt 2>&1
使用正则表达式检查, 原本的 br
语句在何时被替换为 unreachable
, 发现 LoopDeletionPass
将此死循环进行了优化. 分析此 Pass, 发现在 ./llvm/lib/Transforms/Utils/LoopUtils.cpp
文件中的 llvm::deleteDeadLoop
中, 将没有出口的死循环, 通过 Builder.CreateUnreachable();
替换为了 unreachable
指令.
成因分析
此部分优化与 多线程优化
有关. 具体的, 在 C++ 标准中 ` Multi-threaded executions and data races` 章节的第 24 条有如下说明:
The implementation may assume that any thread will eventually do one of the following:
— terminate,
— make a call to a library I/O function,
— access or modify a volatile object, or
— perform a synchronization operation or an atomic operation.
[ Note: This is intended to allow compiler transformations such as removal of empty loops, even when termination cannot be proven. — end note ]
翻译为中文如下:
该实现可能假定任何线程最终都会执行以下操作之一:
— 终止,
— 调用库 I/O 函数,
— 访问或修改易失性对象,或
— 执行同步操作或原子操作。
[注意:这是为了允许编译器转换,例如删除空循环,即使无法证明终止。
即, 多线程中的代码至少会执行一个有意义的指令, 如果一个线程(包括主线程)不会执行这些操作, 如遇到死循环(infinite loop) 等, 编译器可能会将其优化.
在 LLVM
13.0 以上版本, llvm/lib/CodeGen/SelectionDAG/SelectionDAGBuilder.cpp
中, 即编译过程中处理 DAG(Directed Acyclic Graph)
的过程中, visitUnreachable
会处理 IR 代码中的 ``unreachable
指令, 并将其忽略.
void SelectionDAGBuilder::visitUnreachable(const UnreachableInst &I) {
// 判断 -trap-unreachable 选项是否开启, 如果开启则不进行此优化
if (!DAG.getTarget().Options.TrapUnreachable)
return;
// 忽略无返回值调用中的所有不可达指令
if (DAG.getTarget().Options.NoTrapAfterNoreturn) {
const BasicBlock &BB = *I.getParent();
if (&I != &BB.front()) {
BasicBlock::const_iterator PredI =
std::prev(BasicBlock::const_iterator(&I));
if (const CallInst *Call = dyn_cast<CallInst>(&*PredI)) {
if (Call->doesNotReturn())
return;
}
}
}
// 插入 TRAP 指令抛出异常
DAG.setRoot(DAG.getNode(ISD::TRAP, getCurSDLoc(), MVT::Other, DAG.getRoot()));
}
总结
- 为了避免此问题, 可以编译时开启
-trap-unreachable
选项, 编译命令如下:
clang++ -O1 -Wall -mllvm -trap-unreachable main.cpp -o main.elf
- 谨慎对待未定义行为!
-mllvm -trap-unreachable https://godbolt.org/z/oMre33sa1
-mllvm -print-after-all https://godbolt.org/z/8EdhdzEK1
相关讨论
Stackoverflow https://stackoverflow.com/questions/3592557/optimizing-away-a-while1-in-c0x
CPP Standard https://isocpp.org/files/papers/N3690.pdf Page 14 : 24