我正在尝试自己在不同的编译器上进行代码实验。我一直在试图查找在某些函数上禁用异常的优点(通过二进制足迹),并将其与不禁用异常的函数进行比较,我实际上偶然发现了一个奇怪的情况,有异常比没有好。
我一直在使用Matt Godbolt's Compiler Explorer在x86-64 clang 12.0.1上没有任何标记(在GCC上不存在这种奇怪的行为)。
看看这段简单的代码:
auto* allocated_int()
{
return new int{};
}
int main()
{
delete allocated_int();
return 0;
}
非常直接,几乎删除了从函数allocated_int()
返回的已分配指针。
正如预期的那样,二进制空间占用也很小:
allocated_int(): # @allocated_int()
push rbp
mov rbp, rsp
mov edi, 4
call operator new(unsigned long)
mov rcx, rax
mov rax, rcx
mov dword ptr [rcx], 0
pop rbp
ret
同样,非常直接。但是当我将noexcept
关键字应用于allocated_int()
函数时,二进制数据膨胀了。我将在这里应用生成的程序集:
allocated_int(): # @allocated_int()
push rbp
mov rbp, rsp
sub rsp, 16
mov edi, 4
call operator new(unsigned long)
mov rcx, rax
mov qword ptr [rbp - 8], rcx # 8-byte Spill
jmp .LBB0_1
.LBB0_1:
mov rcx, qword ptr [rbp - 8] # 8-byte Reload
mov rax, rcx
mov dword ptr [rcx], 0
add rsp, 16
pop rbp
ret
mov rdi, rax
call __clang_call_terminate
__clang_call_terminate: # @__clang_call_terminate
push rax
call __cxa_begin_catch
call std::terminate()
为什么clang要为我们做这些额外的代码?我没有请求任何其他动作,但调用new()
,我期待二进制文件反映这一点。
谢谢那些能解释的人!
为什么clang要为我们做这些额外的代码?
因为函数的行为是不同的。
我没有请求任何其他操作,但调用new()
通过声明函数noexcept
,您已经请求在异常传播出函数时调用std::terminate
。
第一个程序中的allocated_int
从不调用std::terminate
,而第二个程序中的allocated_int
可以调用std::terminate
。注意,如果您记得启用优化器,添加的代码量会少得多。比较非优化的组装基本上是徒劳的。
可以使用非抛出分配来防止:
return new(std::nothrow) int{};
这确实是一个敏锐的观察,在非抛出函数中做潜在抛出的事情会引入一些额外的工作,如果在潜在抛出的函数中做同样的事情,则不需要做这些工作。
我一直在尝试查找禁用某些函数异常的优点
使用非抛出的优势可能在调用该函数时实现;
没有nothrow
,您的函数只是作为您调用的分配函数的前端。它自己没有任何真正的行为。事实上,在真正的可执行文件中,如果你做了链接时间优化,很有可能它会完全消失。
当您添加noexcept
时,您的代码将默默地转换为大致如下的内容:
auto* allocated_int()
{
try {
return new int{};
}
catch(...) {
terminate();
}
}
您看到的生成的额外代码是捕获异常并在/如果需要时调用terminate
所需要的。