Clang能否合法移除含throw分支的std::malloc调用?附额外疑问
关于Clang优化C++代码的两个问题解答
首先先看示例代码:
#include <cstdlib> #include <vector> namespace { template <typename T> class MyAllocator { public: typedef T value_type; T* allocate(std::size_t n) { T* p = static_cast<T*>(std::malloc(sizeof(T) * n)); if (p == nullptr) throw std::bad_alloc(); return p; } void deallocate(T* p, std::size_t) { std::free(p); } }; void bar() { std::vector<int, MyAllocator<int>> v; v.push_back(49); } } // anonymous namespace int main(int, char**) { ::bar(); return 0; }
当用Clang 6.0.0开启完全优化后,生成的汇编代码非常简洁,只保留了main函数直接返回的逻辑:
main: # @main xor eax, eax ret
接下来针对两个技术疑问逐一解答:
1. 这种优化合法吗?即便存在throw分支,编译器仍能丢弃std::malloc调用?
答案是完全合法,核心依据是C++标准里的「as-if规则」——只要程序的可观察行为(比如控制台输出、文件IO、对外部内存的修改等)没有变化,编译器可以随心所欲地改写代码,哪怕和原代码的执行流程完全不同。
具体到这个例子:
bar()里的vector是局部变量,生命周期仅限于bar()函数内部,整个程序没有任何可观察的输出或外部状态修改——不管内存分配成功还是失败,最终要么程序正常退出(main返回0),要么因异常终止。- 编译器可以判断:如果直接让main返回0,和原程序正常执行路径的可观察结果完全一致;而异常终止的情况,标准允许编译器在不影响可观察行为的前提下忽略这类分支(毕竟异常终止本身也没有可观察的“正确”输出,编译器选择优化掉这条路径并不违反规则)。
至于GCC没有做这个优化,只是它的优化策略相对保守而已,并不代表Clang的做法有问题。
2. 移除匿名命名空间后,编译器生成150行汇编的原因是什么?
这和C++的链接属性直接相关:
- 匿名命名空间里的所有符号(比如
MyAllocator模板、bar()函数)默认是内部链接——编译器可以确定这些符号只会在当前编译单元里被使用,绝对不会被其他.cpp文件引用。所以编译器可以放心地做激进优化:直接把bar()的代码内联到main里,然后发现整个逻辑没有可观察副作用,就直接把所有相关代码都消除了。 - 一旦移除匿名命名空间,
MyAllocator和bar()就变成了外部链接——编译器必须假设这些符号可能会被其他编译单元调用(比如别的文件里可能会声明void bar();然后调用它),所以不能随便消除它们的定义,必须完整保留MyAllocator的allocate/deallocate逻辑、vector的构造/push_back/析构逻辑,以及bar()的完整函数实现,这些代码加起来就有150行左右的汇编了。
内容的提问来源于stack exchange,提问作者BiagioF
相关产品推荐
相关产品推荐

