ein*_*ica 80 c++ assembly abi calling-convention unique-ptr
我正在观看Chandler Carruth在CppCon 2019中的演讲:
在该示例中,他举例说明了您对使用std::unique_ptr<int>
over和会产生多少开销而感到惊讶int*
。该段大约在时间点17:25开始。
您可以看一下他的示例代码对(godbolt.org)的编译结果 -可以看到,确实,编译器似乎不愿意传递unique_ptr值-实际上,底线是只是一个地址-在寄存器内,仅在直接内存中。
Carruth先生在27:00左右提出的观点之一是,C ++ ABI要求按值传递参数(某些但不是全部;也许-非基本类型?而不是在寄存器中。
我的问题:
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);
}
Run Code Online (Sandbox Code Playgroud)
唯一指针:
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));
}
Run Code Online (Sandbox Code Playgroud)
Max*_*kin 46
- 这实际上是ABI要求,还是在某些情况下只是一些悲观?
一个示例是System V应用程序二进制接口AMD64体系结构处理器补充。该ABI适用于64位x86兼容CPU(Linux x86_64体系结构)。在Linux的Solaris,Linux,FreeBSD,macOS和Windows子系统上紧随其后:
如果C ++对象具有非平凡的复制构造函数或非平凡的析构函数,则它将通过不可见的引用传递(该对象在参数列表中被具有INTEGER类的指针替换)。
具有平凡的复制构造函数或非平凡的析构函数的对象无法按值传递,因为此类对象必须具有定义明确的地址。从函数返回对象时,也会遇到类似的问题。
请注意,只有2个通用寄存器可用于通过平凡的复制构造函数和平凡的析构函数传递1个对象,即,只能将sizeof
不大于16个对象的值传递到寄存器中。有关调用约定的详细信息,请参阅Agner Fog的调用约定,尤其是第7.1节“传递和返回对象”。对于在寄存器中传递SIMD类型,有单独的调用约定。
其他CPU架构有不同的ABI。
- 为什么ABI这样?也就是说,如果结构/类的字段适合寄存器,甚至单个寄存器,为什么我们不能在寄存器中传递它呢?
它是实现的详细信息,但是当处理异常时,在堆栈展开期间,自动存储持续时间被破坏的对象必须相对于功能堆栈框架可寻址,因为此时寄存器已被破坏。堆栈展开代码需要对象的地址来调用其析构函数,但寄存器中的对象没有地址。
徒劳地,析构函数对对象进行操作:
一个对象在其构造期间([class.cdtor]),其整个生命周期以及其破坏期间都占据一个存储区域。
如果没有为对象分配可寻址的存储,则该对象不能存在于C ++ 中,因为对象的标识就是其地址。
当需要在寄存器中保存有琐碎复制构造函数的对象的地址时,编译器可以将对象存储到内存中并获取地址。另一方面,如果复制构造函数很重要,则编译器不能仅将其存储到内存中,而是需要调用复制构造函数,该复制构造函数需要一个引用,因此需要对象在寄存器中的地址。调用约定可能无法确定是否在被调用方中内联了复制构造函数。
考虑这种情况的另一种方法是,对于平凡可复制的类型,编译器将对象的值传输到寄存器中,如有必要,可以通过普通内存存储从中恢复对象。例如:
void f(long*);
void g(long a) { f(&a); }
Run Code Online (Sandbox Code Playgroud)
在带有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.
Run Code Online (Sandbox Code Playgroud)
钱德勒·卡鲁思( Chandler Carruth)在他的发人深省的讲话中提到,必须实施一项重大的ABI变更(除其他事项外),以实施可以改善事情的破坏性举动。IMO,如果使用新ABI的功能明确选择具有新的不同链接(例如,在extern "C++20" {}
块中声明它们(可能在用于迁移现有API的新的内联命名空间中)声明),则ABI更改可能不会中断。这样,只有针对具有新链接的新函数声明编译的代码才能使用新的ABI。
请注意,内联被调用函数时,ABI不适用。与链接时代码生成一样,编译器可以内联在其他翻译单元中定义的函数,也可以使用自定义调用约定。
(在@MaximEgorushkin的答案中使用@harold的示例说明了这一点;根据@Yakk的评论进行了更正。)
如果编译:
struct Foo { int bar; };
Foo test(Foo byval) { return byval; }
Run Code Online (Sandbox Code Playgroud)
你得到:
test(Foo):
mov eax, edi
ret
Run Code Online (Sandbox Code Playgroud)
也就是说,将Foo
对象传递到test
寄存器(edi
)中,并返回到寄存器(eax
)中。
当析构函数并非无关紧要时(std::unique_ptr
例如OP的示例),常见的ABI需要放在堆栈上。即使析构函数根本不使用对象的地址,也是如此。
因此,即使在不执行任何操作的析构函数的极端情况下,如果您进行编译,则:
struct Foo2 {
int bar;
~Foo2() { }
};
Foo2 test(Foo2 byval) { return byval; }
Run Code Online (Sandbox Code Playgroud)
你得到:
test(Foo2):
mov edx, DWORD PTR [rsi]
mov rax, rdi
mov DWORD PTR [rdi], edx
ret
Run Code Online (Sandbox Code Playgroud)
与无用的加载和存储。