从unique_ptr看函数调用约定

本文带着大家一起看下关于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类型使用内存传递
  • 类或者结构体:
    1. 有大于等于4个八字节成员,使用内存传递
    2. 有non-trivial的拷贝构造和non-trivial析构函数,使用内存传递
    3. 否则将结构按照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
下面通过具体的例子来说明如何将`unique_ptr`作为函数参数,并解释其作用以及注意事项。 ### 背景知识 `std::unique_ptr` 是 C++ 标准库提供的一种智能指针,用于管理动态分配的对象。它的核心特点是“独占所有权”,也就是说,在任意时刻只有一个 `unique_ptr` 拥有某个对象的所有权。当你尝试将 `unique_ptr` 传递给另一个变量或函数时,默认情况下会触发移动语义(move semantics),这意味着原 `unique_ptr` 将失去对其所指向对象的控制。 --- ### 示例代码 #### 场景描述 假设我们需要设计一个程序,其中有一个函数接收并处理由 `unique_ptr` 管理的对象。我们将展示三种常见的用法: 1. **按值传递** 2. **按右值引用 (&&) 传递** 3. **按常量引用 (const &) 读取** --- #### 1. 按值传递 (`unique_ptr` 的所有权转移) 在这种模式下,当我们把 `unique_ptr` 传入函数时,所有权从调用方转移到目标函数内部。 ```cpp #include <iostream> #include <memory> // 函数原型:接收 unique_ptr 并获取其所有权 void processObject(std::unique_ptr<int> ptr) { if (!ptr) return; // 检查空指针 std::cout << "Value inside function: " << *ptr << std::endl; } int main() { auto up = std::make_unique<int>(42); // 创建一个整数类型的 unique_ptr // 所有权移交给函数 processObject(std::move(up)); if (!up) { // 移交之后,外部 unique_ptr 已经为空 std::cout << "Original pointer is now null." << std::endl; } return 0; } ``` **运行结果**: ``` Value inside function: 42 Original pointer is now null. ``` --- #### 2. 按右值引用 (&&) 传递 如果我们只希望临时借用 `unique_ptr` 对象而不完全接管它,则可以采用这种方式。 ```cpp #include <iostream> #include <memory> // 使用右值引用来避免拷贝开销同时保留所有权在外围范围。 void inspectAndModify(std::unique_ptr<int>&& ptr) noexcept(false){ ++(*ptr); } int main(){ auto ptrInt = std::make_unique<int>(8); inspectAndModify(std::move(ptrInt)); std::cout<<"Modified value:"<<*ptrInt<<"\n"; return 0; } ``` 注意这里存在一个小问题——修改完成后,原始 `ptrInt` 应已变为无效状态。因此上述实现并不完美;一般不会这么操作而是推荐其他替代方案如返回新创建的对象等。 --- #### 3. 常量引用访问 如果你只是想观察而不想改变也不打算夺取资源的话,可以用 const 引用的形式来做: ```cpp #include <iostream> #include <memory> void printValue(const std::unique_ptr<int>& ptr) { if (ptr) { std::cout << "*ptr = " << *ptr << '\n'; } else { std::cout << "Null pointer\n"; } } int main() { auto uPtr = std::make_unique<int>(77); printValue(uPtr); // 输出:*ptr=77 uPtr.reset(); // 销毁内容 printValue(uPtr); // 输出: Null pointer return 0; } ``` --- ### 总结 - 当你需要移交 `unique_ptr` 的所有权时,可以选择按值或者右值引用的方式; - 若仅仅是查询相关信息则适合使用常量引用来减少不必要的性能消耗及风险规避措施.
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值