与普通指针相比,按值传递“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 对于任何接受非平凡参数的函数都是不可能的。
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 对于任何接受非平凡参数的函数都是不可能的。