与普通指针相比,通过值传递`unique_ptr`会产生性能损失吗?

Jen*_*ens 10 c++ overhead unique-ptr

常识是std::unique_ptr不引入性能损失(并且不使用删除器参数时不会造成内存损失),但我最近偶然发现了一个讨论,显示它实际上引入了一个额外的间接,因为unique_ptr无法在平台上的寄存器中传递安腾ABI.发布的示例类似于

#include <memory>

int foo(std::unique_ptr<int> u) {
    return *u;
}

int boo(int* i) {
    return *i;
}
Run Code Online (Sandbox Code Playgroud)

与boo相比,这在foo中生成了额外的汇编指令.

foo(std::unique_ptr<int, std::default_delete<int> >):
        mov     rax, QWORD PTR [rdi]
        mov     eax, DWORD PTR [rax]
        ret
boo(int*):
        mov     eax, DWORD PTR [rdi]
        ret
Run Code Online (Sandbox Code Playgroud)

解释是Itanium ABI要求unique_ptr由于非平凡的构造函数而不能在寄存器中传递,因此它在堆栈上创建,然后该对象的地址在寄存器中传递.

我知道这并没有真正影响现代PC平台的性能,但我想知道是否有人可以提供更多详细信息,说明为什么不能将其复制到寄存器中.由于零成本抽象是C++的主要目标之一,我想知道这是否已经在标准化过程中被讨论为可接受的偏差或者是否是实施质量问题.在考虑其优势时,性能损失肯定很小,特别是在现代PC平台上.

评论者指出,这两个函数并不完全等效,因此比较存在缺陷,因为它foo也会调用unique_ptr参数上的删除函数,但boo不会释放内存.但是,我只对传递一个unique_ptrby-value与传递一个普通指针所产生的差异感兴趣.我修改了示例代码并包含了一个delete释放普通指针的调用 ; 调用是在调用者中,因为unique_ptr调用者的上下文中也调用了删除函数,以使生成的代码更加相同.此外,手册delete也会检查,ptr != nullptr因为析构函数也会这样做.但是,foo不会在寄存器中传递参数并且必须进行间接访问.

我也想知道为什么编译器nullptr在调用之前不会忽略检查,operator delete因为无论如何这被定义为noop.我想这unique_ptr可能是专门用于默认删除器,不在析构函数中执行检查,但这将是一个非常小的微优化.

Ser*_*eyA 5

System V ABI使用Itanium C ++ ABI并对其进行引用。特别是,C ++ Itanium ABI指定

如果出于调用目的参数类型很重要,则调用者必须为临时对象分配空间,并通过引用传递该临时对象。

特别:

...

如果类型具有非平凡的析构函数,则在将全表达式括起来之后,调用者会在控制权返回给它之后(包括调用者抛出异常时)调用该析构函数。

因此,“ 为什么不将其传递到寄存器这个问题的简单答案是“ 因为它不能 ”。

现在,一个有趣的问题可能是“ 为什么C ++ Itanium ABI决定这样做 ”。

尽管我不会声称自己具有基本的知识,但是我想到了两点:

  • 如果该函数的参数是临时变量,则允许省略复制
  • 这使尾部呼叫优化功能更加强大。如果被调用方需要调用其参数的析构函数,则对于任何接受非平凡参数的函数,TCO都是不可能的。