为何std::unique_ptr与int*调用的生成代码差异大,无法优化delete?
为什么std::unique_ptr版本的代码无法优化到与裸指针版本一致?
两种函数定义与调用方式
接收std::unique_ptr的函数
void foo(std::unique_ptr<int>); // 调用代码 foo(std::make_unique<int>(0));
接收裸指针的函数
void foo(int*); // 调用代码 foo(new int{0});
预期与实际的汇编差异
我原本预期两者生成的汇编代码几乎完全相同:毕竟std::make_unique本质就是调用new int{0},后续的移动构造、临时对象销毁(此时已转移所有权,指针应为nullptr)都应该被优化掉,包括~unique_ptr()中的nullptr检查。
但实际生成的汇编差异显著:
std::unique_ptr版本的汇编(正常执行路径)
test(): push rbx sub rsp, 16 mov edi, 4 call operator new(unsigned long)@PLT mov dword ptr [rax], 0 mov qword ptr [rsp + 8], rax lea rdi, [rsp + 8] call foo(std::unique_ptr<int, std::default_delete<int>>)@PLT mov rdi, qword ptr [rsp + 8] test rdi, rdi je .LBB0_3 mov esi, 4 call operator delete(void*, unsigned long)@PLT .LBB0_3: add rsp, 16 pop rbx ret
裸指针版本的优化后汇编
test(): push rax mov edi, 4 call operator new(unsigned long)@PLT mov dword ptr [rax], 0 mov rdi, rax pop rax jmp foo(int*)@PLT
当使用自定义空删除器std::unique_ptr<int, Nop>时,生成的代码符合预期(与裸指针版本几乎一致)。另外,将foo()声明为noexcept能减少部分生成代码,但仍会保留潜在的delete调用分支。
核心疑问
为什么优化器无法看穿所有权转移,从而优化掉多余的delete调用?我原本的预期执行流程是:
std::make_unique内部调用new int(0),若抛出异常则无需执行delete;- 参数被移动构造到
foo中(或被返回值优化RVO省略),该操作是noexcept的; foo执行完毕后会销毁自己的unique_ptr参数,接管所有权;- 原临时
unique_ptr已转移所有权(指针为nullptr),其销毁时无需执行delete。
原因解析
这本质是因为优化器无法跨函数边界保证foo一定会正确接管所有权:
- 虽然我们知道
std::unique_ptr的移动构造会转移所有权、原对象变为nullptr,但优化器不能假设foo的实现一定会遵循规范——比如foo可能没有移动构造该unique_ptr,甚至可能手动将指针重置回原临时对象(这种行为不符合最佳实践,但优化器必须考虑所有合法的代码可能性); - 对于默认删除器
std::default_delete<int>,优化器需要严格保证:如果临时unique_ptr销毁时指针非空,必须调用delete。由于无法确定foo是否会修改该指针的状态,所以必须保留nullptr检查和对应的delete分支; - 当使用自定义空删除器时,无论指针是否为空,
unique_ptr销毁时都不会执行任何操作,优化器可以直接去掉所有多余分支,生成与裸指针一致的代码; - 标记
foo为noexcept能减少异常处理相关的冗余代码,但依然无法消除对foo是否会修改指针状态的不确定性,因此delete分支无法被完全优化掉。
内容的提问来源于stack exchange,提问作者Ben
相关产品推荐
相关产品推荐

