gpt4 book ai didi

c++ - 为什么 T* 可以在寄存器中传递,但 unique_ptr 却不能?

转载 作者:行者123 更新时间:2023-12-02 09:21:13 26 4
gpt4 key购买 nike

我正在观看 Chandler Carruth 在 CppCon 2019 上的演讲:

There are no Zero-Cost Abstractions

其中,他举了一个例子,说明他对使用 std::unique_ptr<int> 产生的开销感到惊讶。超过 int* ;该片段大约在时间点 17:25 开始。

您可以查看compilation results他的示例片段对(godbolt.org) - 见证这一点,确实,编译器似乎不愿意传递 unique_ptr 值 - 事实上,它在底线只是一个地址 - 仅在寄存器内在直接内存中。

Carruth 先生在 27:00 左右提出的观点之一是,C++ ABI 需要按值参数(部分但不是全部;也许 - 非原始类型?非平凡可构造类型?)进行传递在内存中而不是在寄存器中。

我的问题:

  1. 这实际上是某些平台上的 ABI 要求吗? (哪个?)或者也许这只是某些情况下的一些悲观情绪?
  2. 为什么 ABI 是这样的?也就是说,如果结构/类的字段适合寄存器,甚至单个寄存器 - 为什么我们不能在该寄存器中传递它?
  3. C++ 标准委员会近年来或曾经讨论过这一点吗?
<小时/>

PS - 不要让这个问题没有代码:

普通指针:

void bar(int* ptr) noexcept;
void baz(int* ptr) noexcept;

void foo(int* ptr) noexcept {
if (*ptr > 42) {
bar(ptr);
*ptr = 42;
}
baz(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));
}

最佳答案

  1. Is this actually an ABI requirement, or maybe it's just some pessimization in certain scenarios?

一个例子是 System V Application Binary Interface AMD64 Architecture Processor Supplement 。此 ABI 适用于 64 位 x86 兼容 CPU(Linux x86_64 架构)。在 Solaris、Linux、FreeBSD、macOS、Linux 的 Windows 子系统上遵循:

If a C++ object has either a non-trivial copy constructor or a non-trivial destructor, it is passed by invisible reference (the object is replaced in the parameter list by a pointer that has class INTEGER).

An object with either a non-trivial copy constructor or a non-trivial destructor cannot be passed by value because such objects must have well defined addresses. Similar issues apply when returning an object from a function.

请注意,只有 2 个通用寄存器可用于传递 1 个带有普通复制构造函数和普通析构函数的对象,即只能传递 sizeof 不大于 16 的对象值寄存器。请参阅Calling conventions by Agner Fog有关调用约定的详细处理,特别是 §7.1 传递和返回对象。在寄存器中传递 SIMD 类型有单独的调用约定。

其他 CPU 架构有不同的 ABI。

<小时/>

还有Itanium C++ ABI大多数编译器都遵守(除了 MSVC), requires :

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.

A type is considered non-trivial for the purposes of calls if:

  • it has a non-trivial copy constructor, move constructor, or destructor, or
  • all of its copy and move constructors are deleted.

This definition, as applied to class types, is intended to be the complement of the definition in [class.temporary]p3 of types for which an extra temporary is allowed when passing or returning a type. A type which is trivial for the purposes of the ABI will be passed and returned according to the rules of the base C ABI, e.g. in registers; often this has the effect of performing a trivial copy of the type.

<小时/>
  1. Why is the ABI like that? That is, if the fields of a struct/class fit within registers, or even a single register - why should we not be able to pass it within that register?

这是一个实现细节,但是当处理异常时,在堆栈展开期间,自动存储持续时间被销毁的对象必须相对于函数堆栈帧是可寻址的,因为此时寄存器已被破坏。堆栈展开代码需要对象的地址来调用其析构函数,但寄存器中的对象没有地址。

迂腐地,destructors operate on objects :

An object occupies a region of storage in its period of construction ([class.cdtor]), throughout its lifetime, and in its period of destruction.

如果没有为其分配可寻址存储空间,则对象不能存在于 C++ 中,因为 object's identity is its address .

当需要使用保存在寄存器中的简单复制构造函数的对象的地址时,编译器可以将对象存储到内存中并获取地址。另一方面,如果复制构造函数很重要,则编译器不能仅将其存储到内存中,而是需要调用复制构造函数,该复制构造函数需要引用,因此需要寄存器中对象的地址。调用约定可能不依赖于复制构造函数是否内联在被调用者中。

考虑这个问题的另一种方法是,对于普通可复制类型,编译器会在寄存器中传输对象的,如果需要,可以通过普通内存存储从中恢复对象。例如:

void f(long*);
void g(long a) { f(&a); }

在具有 System V ABI 的 x86_64 上编译为:

g(long):                             // Argument a is in rdi.
push rax // Align stack, faster sub rsp, 8.
mov qword ptr [rsp], rdi // Store the value of a in rdi into the stack to create an object.
mov rdi, rsp // Load the address of the object on the stack into rdi.
call f(long*) // Call f with the address in rdi.
pop rax // Faster add rsp, 8.
ret // The destructor of the stack object is trivial, no code to emit.
<小时/>

钱德勒·卡鲁斯 (Chandler Carruth) 的发人深省的演讲 mentions可能有必要(除其他外)进行突破性的 ABI 更改,以实现可以改善情况的破坏性举措。 IMO,如果使用新 ABI 的函数明确选择加入新的不同链接,则 ABI 更改可能不会造成破坏。在 extern "C++20"{} block 中声明它们(可能在用于迁移现有 API 的新内联命名空间中)。这样,只有针对具有新链接的新函数声明进行编译的代码才能使用新的 ABI。

请注意,当被调用函数已内联时,ABI 不适用。除了链接时代码生成之外,编译器还可以内联其他翻译单元中定义的函数或使用自定义调用约定。

关于c++ - 为什么 T* 可以在寄存器中传递,但 unique_ptr<T> 却不能?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/53331010/

26 4 0
Copyright 2021 - 2024 cfsdn All Rights Reserved 蜀ICP备2022000587号
广告合作:1813099741@qq.com 6ren.com