为什么T *可以在寄存器中传递,但unique_ptr <T>无法传递?

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要求按值传递参数(某些但不是全部;也许-非基本类型?而不是在寄存器中。

我的问题:

  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);
}
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

  1. 这实际上是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。


  1. 为什么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不适用。与链接时代码生成一样,编译器可以内联在其他翻译单元中定义的函数,也可以使用自定义调用约定。


ein*_*ica 8

使用常见的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)

与无用的加载和存储。

  • “琐碎的破坏者[CITATION NEEDED]”显然是错误的;如果没有代码实际依赖于该地址,则仿佛表示该地址不必在实际的机器上存在。该地址必须存在于*抽象机器中*,但是抽象机器中对实际机器没有任何影响的事物是*好像*被允许消除的事物。 (3认同)
  • @einpoklum在标准中,没有任何状态寄存器存在。register关键字仅指出“您不能使用该地址”。就标准而言,只有一台抽象机。“好像”意味着任何实际的机器实现都只需要“像”抽象机器一样地工作,直到标准未定义的行为为止。现在,在寄存器中放置一个对象存在着非常具有挑战性的问题,每个人都对此进行了广泛讨论。此外,该标准也未讨论的调用约定具有实际需求。 (2认同)
  • @curiousguy:我们正在讨论抽象机器的任意实现。您可以决定将地址空间的一部分保留给寄存器中的内容,并且内存从某个非零地址开始。 (2认同)