序
本文带着大家一起看下关于unique_ptr作为参数传递时,和裸指针在开销上的对比,参照原视频https://www.youtube.com/watch?v=rHIkrotSwcc中。
如果感兴趣还请点个赞,增加一下创作动力🙏。
ABI
ABI(application binary interface)翻译为应用二进制接口,是指两程序模块间的接口,相对于API来说更加更加底层,依赖硬件。比如说函数调用约定。采用某种ABI通常由编译器,操作系统或者运行时库来决定。
ABI大致包括以下几个方面:
- 数据类型的大小、布局和对齐
- 函数调用约定(控制着函数的参数如何传送以及如何接受返回值)
- 系统调用
- 目标文件的二进制格式,程序库等
这里我们也只是借助unique_ptr把函数调用约定拿出来说
例子
源码是这样的
// raw pointer
void bar(int* ptr) noexcept;
void baz(int* ptr) noexcept;
void foo(int* ptr) noexcept {
if (*ptr > 42) {
bar(ptr);
*ptr = 42;
}
baz(ptr);
}
// unique_ptr
using std::unique_ptr;
void bar(int* ptr) noexcept;
void baz(unique_ptr<int> ptr) noexcept;
void foo(unique_ptr<int> ptr) noexcept {
if (*ptr > 42) {
bar(ptr.get());
*ptr = 42;
}
baz(std::move(ptr));
}
视频中展示的汇编代码是这样的:
其中红色标出,使用unique_ptr会比普通的裸指针多两次内存操作。那谈到内存,就会牵扯出一级缓存,二级缓存,缓存命中,内存带宽等等概念。也就是说性能上是有损失的,视频中也提出了这和ABI有关,我们也去查阅下函数调用约定来解密下。
函数调用约定
指针和对象
以x86_64 linux下的64位操作系统作为讨论。
如果一个C++的对象有non-trivial的拷贝构造函数或者non-trivial的析构函数,会在栈上分配空间,参数传递时使用内存传递。
如果裸指针类型,可以直接使用寄存器传递,使用的寄存器顺序为%rdi, %rsi, %rdx, %rcx, %r8, %r9
(也即通用寄存器)
那么什么原因呢?
针对于有non-trivia析构函数来说,当函数中异常抛出时,要进行栈展开,去调用析构函数,而此时就是需要到该对象的地址,从而执行析构函数内部逻辑等。
针对于有non-trivial的拷贝构造函数道理也是类似的,在函数内部进行拷贝时,不能像trivial那样直接拷贝,拷贝构造函数中需要执行相应的逻辑。
其他类型参数传递
我们扩展下其他类型,方便大家之后的使用:
- _Bool, char, short, int, long, long long, 指针类型使用通用寄存器传递参数
- float, double使用向量寄存器传递参数(%xmm0 到 %xmm7)
- long double类型使用内存传递
- 类或者结构体:
- 有大于等于4个八字节成员,使用内存传递
- 有non-trivial的拷贝构造和non-trivial析构函数,使用内存传递
- 否则将结构按照2字段分类,直到每个划分是8字节,如果是内嵌结构体,继续递归划分。
a. 如果划分两个不相等,则内存传递。相等则继续
b. 如果划分中的一个内存传递,则整个结构体是内存传递,否则继续
c. 然后划分的成员按照通用寄存器传递或者向量寄存器传递
类的第三条有点难理解:
struct BB {
int ba;
int bb;
};
struct AA {
BB a;
BB b;
double c;
};
void foo() noexcept {
AA a;
test(a);
}
这里BB是八个字节,传递AA对象时,我们划分为2部分,也就是AA的a和b划分在一起是16字节,c单独一个是8字节,该结构体使用内存传递。[3.a]
struct AA {
BB a;
double c;
};
将AA改成这样,划分两部分相等,且其中没有内存传递的成员,则在调用test时,a.a使用rdi寄存器传递,a.c使用xmm0寄存器传递。[3.c]
struct BB {
~BB() {} // here
int ba;
int bb;
};
把BB类改写成这样,增加了析构函数,这样AA中a成员就是内存传递,那么AA整个对象就是内存传递。
总结
本文中其实是两个主题,一个是unique_ptr在参数传递和裸指针对比,unique_ptr使用内存传递,因为unique_ptr内部本身就是封装了指针,这就导致unique_ptr传递时会多一层。也是参考视频中提到的可能没有零成本的抽象吧。
另外一个就是我们扩展了下其他类型的参数是走寄存器传递还是内存传递,也举例说明了下。不过这里省略了SIMD类型的参数。
其中可能那里不对,请大家指出,共同进步
ref
- https://www.youtube.com/watch?v=rHIkrotSwcc
- https://www.uclibc.org/docs/psABI-x86_64.pdf
- https://stackoverflow.com/questions/58339165/why-can-a-t-be-passed-in-register-but-a-unique-ptrt-cannot
- https://en.wikipedia.org/wiki/Application_binary_interface