c++函数与noexcept在这种情况下实际上是较慢的?



我正在尝试自己在不同的编译器上进行代码实验。我一直在试图查找在某些函数上禁用异常的优点(通过二进制足迹),并将其与不禁用异常的函数进行比较,我实际上偶然发现了一个奇怪的情况,有异常比没有好。

我一直在使用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所需要的。

最新更新