问题描述
我正在观看Chandler Carruth在CppCon 2019上的演讲:
I'm watching Chandler Carruth's talk in CppCon 2019:
中,他给出了一个示例,说明了在int*
上使用std::unique_ptr<int>
会产生多少开销,这让他感到惊讶;该片段大约在时间点17:25开始.
in it, he gives the example of how he was surprised by just how much overhead you incur by using an std::unique_ptr<int>
over an int*
; that segment starts about at time point 17:25.
您可以查看他的示例代码对(godbolt)的编译结果. org)-确实可以看到,编译器似乎不愿意传递unique_ptr值-实际上,在底行中这只是一个地址-在寄存器内部,仅在直接内存中.
You can have a look at the compilation results of his example pair-of-snippets (godbolt.org) - to witness that, indeed, it seems the compiler is not willing to pass the unique_ptr value - which in fact in the bottom line is just an address - inside a register, only in straight memory.
Carruth先生在27:00左右提出的观点之一是,C ++ ABI需要按值传递参数(某些但不是全部;也许是-非基本类型?内存中而不是寄存器中.
One of the points Mr. Carruth makes at around 27:00 is that the C++ ABI requires by-value parameters (some but not all; perhaps - non-primitive types? non-trivially-constructible types?) to be passed in-memory rather than within a register.
我的问题:
- 在某些平台上,这实际上是ABI要求吗? (哪个?)或者在某些情况下可能只是一些悲观?
- 为什么ABI这样?也就是说,如果结构/类的字段适合寄存器,甚至单个寄存器,为什么我们不能在寄存器中传递它呢?
- 近年来,C ++标准委员会是否讨论过这一点?
PS-以免不带任何代码离开这个问题:
PS - So as not to leave this question with no code:
普通指针:
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));
}
推荐答案
- 这实际上是ABI要求,还是在某些情况下只是一些悲观?
一个示例是系统V应用程序二进制接口AMD64体系结构处理器补充程序.该ABI适用于64位x86兼容CPU(Linux x86_64体系结构).在Linux的Solaris,Linux,FreeBSD,macOS和Windows子系统上紧随其后:
One example is System V Application Binary Interface AMD64 Architecture Processor Supplement. This ABI is for 64-bit x86-compatible CPUs (Linux x86_64 architecure). It is followed on Solaris, Linux, FreeBSD, macOS, Windows Subsystem for Linux:
具有非平凡副本构造函数或非平凡析构函数的对象不能为 通过值传递,因为此类对象必须具有定义明确的地址.类似的问题适用 从函数返回对象时.
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的对象的值可以在寄存器中传递.请参阅 Agner Fog的呼叫约定,以详细了解呼叫约定,尤其是§ 7.1传递和返回物体.在寄存器中传递SIMD类型有单独的调用约定.
Note, that only 2 general purpose registers can be used for passing 1 object with a trivial copy constructor and a trivial destructor, i.e. only values of objects with sizeof
no greater than 16 can be passed in registers. See Calling conventions by Agner Fog for a detailed treatment of the calling conventions, in particular §7.1 Passing and returning objects. There are separate calling conventions for passing SIMD types in registers.
其他CPU架构有不同的ABI.
There are different ABIs for other CPU architectures.
还有 Itanium C ++ ABI ,大多数编译器都遵守(除了MSVC),要求:
There is also Itanium C++ ABI which most compilers comply with (apart from MSVC), which requires:
在以下情况下,出于调用目的,一种类型被认为是不平凡的:
A type is considered non-trivial for the purposes of calls if:
- 它具有非平凡的副本构造函数,move构造函数或析构函数,或者
- 其所有复制和移动构造函数均被删除.
此定义(应用于类类型)旨在作为[class.temporary] p3中类型的定义的补充,在传递或返回类型时,允许为该类型提供一个额外的临时值.对于ABI而言,微不足道的类型将根据基本C ABI的规则进行传递和返回,例如在寄存器中;通常,这会执行普通类型的复制.
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.
- 为什么ABI这样?也就是说,如果结构/类的字段适合寄存器,甚至单个寄存器,为什么我们不能在寄存器中传递它呢?
这是一个实现细节,但是在堆栈展开时处理异常时,具有自动存储持续时间的对象必须相对于功能堆栈框架可寻址,因为此时寄存器已被破坏.堆栈展开代码需要对象的地址来调用其析构函数,但寄存器中的对象没有地址.
It is an implementation detail, but when an exception is handled, during stack unwinding, the objects with automatic storage duration being destroyed must be addressable relative to the function stack frame because the registers have been clobbered by that time. Stack unwinding code needs objects' addresses to invoke their destructors but objects in registers do not have an address.
理想地,析构函数在对象上进行操作:
,如果没有为其分配可寻址存储,则对象在C ++中将不存在,因为对象的身份是其地址.
and an object cannot exist in C++ if no addressable storage is allocated for it because object's identity is its address.
当需要在寄存器中保存有简单复制构造函数的对象的地址时,编译器可以将对象存储到内存中并获取地址.另一方面,如果复制构造函数很重要,则编译器不能仅将其存储到内存中,而是需要调用复制构造函数,该复制构造函数需要一个引用,因此需要对象在寄存器中的地址.调用约定可能无法取决于复制构造函数是否在被调用者中内联.
When an address of an object with a trivial copy constructor kept in registers is needed the compiler can just store the object into memory and obtain the address. If the copy constructor is non-trivial, on the other hand, the compiler cannot just store it into memory, it rather needs to call the copy constructor which takes a reference and hence requires the address of the object in the registers. The calling convention probably cannot depend whether the copy constructor was inlined in the callee or not.
对此进行考虑的另一种方法是,对于普通可复制类型,编译器将对象的 value 传输到寄存器中,如有必要,可以通过普通内存存储从中恢复对象.例如:
Another way to think about this, is that for trivially copyable types the compiler transfers the value of an object in registers, from which an object can be recovered by plain memory stores if necessary. E.g.:
void f(long*);
void g(long a) { f(&a); }
使用System V ABI在x86_64上的
编译为:
on x86_64 with System V ABI compiles into:
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)发人深省的讲话提到可能需要对ABI进行重大更改(除其他事项外)实施可以改善事情的破坏性举动. IMO,如果使用新ABI的功能明确选择具有新的不同链接,例如ABI,则ABI更改可能是不间断的.在extern "C++20" {}
块中声明它们(可能在用于迁移现有API的新的内联命名空间中).这样,只有针对具有新链接的新函数声明编译的代码才能使用新的ABI.
In his thought-provoking talk Chandler Carruth mentions that a breaking ABI change may be necessary (among other things) to implement the destructive move that could improve things. IMO, the ABI change could be non-breaking if the functions using the new ABI explicitly opt-in to have a new different linkage, e.g. declare them in extern "C++20" {}
block (possibly, in a new inline namespace for migrating existing APIs). So that only the code compiled against the new function declarations with the new linkage can use the new ABI.
请注意,内联被调用函数时,ABI不适用.除了生成链接时代码外,编译器还可以内联在其他翻译单元中定义的函数或使用自定义调用约定.
Note that ABI doesn't apply when the called function has been inlined. As well as with link-time code generation the compiler can inline functions defined in other translation units or use custom calling conventions.
这篇关于为什么T *可以在寄存器中传递,而unique_ptr< T>却不能传递?不能?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持!