与普通指针相比,按值传递“unique_ptr”是否会降低性能?

Does passing a `unique_ptr` by value have a performance penalty compared to a plain pointer?

Common wisdom is that std::unique_ptr does not introduce a performance penalty (),但我最近偶然发现了一个讨论,该讨论表明它实际上引入了一个额外的间接寻址,因为 unique_ptr 无法在具有 Itanium ABI 的平台上的寄存器中传递。发布的示例类似于

#include <memory>

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

int boo(int* i) {
    return *i;
}

Which generates an additional assembler instruction in foo compared to boo.

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

解释是 Itanium ABI 要求 unique_ptr 不应该在寄存器中传递,因为非平凡的构造函数,所以它在堆栈上创建,然后传递这个对象的地址在寄存器中。

我知道这不会真正影响现代 PC 平台的性能,但我想知道是否有人可以提供更多详细信息,说明为什么不能将其复制到寄存器中的原因。由于零成本抽象是 C++ 的主要目标之一,我想知道这是否已在标准化过程中作为可接受的偏差进行讨论,或者它是否是实现质量问题。考虑到好处时,性能损失肯定足够小,尤其是在现代 PC 平台上。

评论者指出这两个函数并不完全等价,因此比较存在缺陷,因为 foo 也会调用 unique_ptr 参数上的删除器,但 boo 不会释放内存。但是,我只对按值传递 unique_ptr 与传递普通指针相比所产生的差异感兴趣。 I've modified the example code and included a call to delete to free the plain pointer;该调用在调用者中,因为 unique_ptr 的删除器也在调用者的上下文中被调用以使生成的代码更加相同。另外,手册delete也会检查ptr != nullptr,因为析构函数也是这样做的。不过,foo 不会在寄存器中传递参数,必须 进行间接访问。

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

System V ABI 使用 Itanium C++ ABI 并引用它。特别是,C++ Itanium ABI 指定

If the parameter type is non-trivial for the purposes of calls, the caller must allocate space for a temporary and pass that temporary by reference.

Specifically:

...

If the type has a non-trivial destructor, the caller calls that destructor after control returns to it (including when the caller throws an exception), at the end of enclosing full-expression.

所以对于“为什么它没有被传递到寄存器”这个问题的简单回答是“因为它不能”。

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

虽然我不会声称我对基本原理有深入的了解,但我想到了两件事:

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