C++在x86-64平台上:什么时候会使用寄存器传递和返回结构体/类?

17

在Linux上假设使用x86-64 ABI,C ++中的struct何时通过寄存器传递给函数而不是放置在栈上?它们何时通过寄存器返回?这个答案对类是否有所改变呢?

如果简化回答有助于理解,可以假设只有一个参数/返回值并且没有浮点值。


5
我担心唯一的回答只能是“这取决于”。这取决于编译器、优化级别、结构/类大小、编译器心情等因素。 - YSC
1
我认为这完全不是真的。由于单独编译的代码必须能够相互操作,ABI 准确地指定了给定函数签名的函数调用方式。 - jacobsa
它不取决于编译器的心情:所有定义都在这里(https://software.intel.com/sites/default/files/article/402129/mpx-linux64-abi.pdf)。虽然算法有点复杂(除了第四步描述不清楚),但并不复杂。完整的答案将涉及大量示例。 - Margaret Bloom
实际上,GCC和clang正在实现的是这个版本 - Margaret Bloom
@MargaretBloom:那些链接是旧版本。当前版本是0.99.8(git修订版r252),发布于2016年4月。请参见https://dev59.com/J2Ml5IYBdhLWcg3wyJYe#40348010。 - Peter Cordes
显示剩余2条评论
2个回答

23

ABI规范在这里定义。
可用更新版本在这里

我假定读者习惯于文档术语,并且能够对原始类型进行分类。


如果对象大小大于两个八字节,则在内存中传递:

struct foo
{
    unsigned long long a;
    unsigned long long b;
    unsigned long long c;               //Commenting this gives mov rax, rdi
};

unsigned long long foo(struct foo f)
{ 
  return f.a;                           //mov     rax, QWORD PTR [rsp+8]
} 

如果它不是POD类型,那么它将在内存中传递:

struct foo
{
    unsigned long long a;
    foo(const struct foo& rhs){}            //Commenting this gives mov rax, rdi
};

unsigned long long foo(struct foo f)
{
  return f.a;                               //mov     rax, QWORD PTR [rdi]
}

这里正在实现拷贝省略

如果它包含未对齐字段,则在内存中传递:

struct __attribute__((packed)) foo         //Removing packed gives mov rax, rsi
{
    char b;
    unsigned long long a;
};

unsigned long long foo(struct foo f)
{
  return f.a;                             //mov     rax, QWORD PTR [rsp+9]
}
如果以上条件都不成立,则会考虑对象的字段。
如果其中一个字段本身是结构体/类,则递归应用该过程。
目标是对对象中的每个8字节(8B)进行分类。
考虑每个8B的字段类别。
请注意,由于上述对齐要求,整数数量的字段总是完全占据一个8B。
设C为8B的类别,D为所考虑字段的类别。
令new_class被伪定义为:
cls new_class(cls D, cls C)
{
   if (D == NO_CLASS)
      return C;

   if (D == MEMORY || C == MEMORY)
      return MEMORY;

   if (D == INTEGER || C == INTEGER)
      return INTEGER;

   if (D == X87 || C == X87 || D == X87UP || C == X87UP)
      return MEMORY;

   return SSE;
}

然后,计算 8B 的类别如下:

C = NO_CLASS;

for (field f : fields)
{
    D = get_field_class(f);        //Note this may recursively call this proc
    C = new_class(D, C);
}

一旦我们确定了每个8B的类别,比如C1和C2,那么

if (C1 == MEMORY || C2 == MEMORY)
    C1 = C2 = MEMORY;

if (C2 == SSEUP AND C1 != SSE)
   C2 = SSE;

注意 这是我根据ABI文件给出的算法的解释。


示例

struct foo
{
    unsigned long long a;
    long double b;
};

unsigned long long foo(struct foo f)
{
  return f.a;
}

8个字节和它们对应的类型

第一个8B:a 第二个8B:b

a 是整数类型,所以第一个8B是整数类型。 b 是X87和X87UP类型,所以第二个8B是存储器类型。 最终类别对于这两个8Bs都是存储器类型。


例子

struct foo
{
    double a;
    long long b;
};

long long foo(struct foo f)
{
  return f.b;                     //mov rax, rdi
}

8B的分类及其领域

第一个8Ba 第二个8Bb

a是SSE,所以第一个8B是SSE。
b是INTEGER,所以第二个8B是INTEGER。

最终的类别是计算得出的。


返回值

根据它们的类别相应地返回值:

  • MEMORY
    调用者向函数传递一个隐藏的第一个参数,以将结果存储在其中。
    在C++中,这通常涉及复制省略/返回值优化。 必须将此地址返回到eax,从而将MEMORY类“按引用”返回给隐藏的调用者分配的缓冲区。

    如果类型具有MEMORY类,则调用者提供返回值的空间并将此存储器的地址作为第一个参数(就好像它是函数的第一个参数)。 实际上,此地址成为“隐藏”的第一个参数。 在返回时,%rax将包含由调用者在%rdi中传入的地址。

  • INTEGERPOINTER
    需要使用寄存器raxrdx

  • SSESSEUP
    需要使用寄存器xmm0xmm1

  • X87X87UP
    需要使用寄存器st0


POD

技术定义在这里

来自ABI的定义如下。

如果它是一个隐式声明的默认de/constructor,并且:
   • 它的类没有虚函数和虚基类,且
   • 其类的所有直接基类具有微不足道的de/constructors,以及
   • 对于其类的所有非静态数据成员,其类型为类类型(或其数组),那么该类的每个这样的类都具有微不足道的de/constructor。


注意,每个8B都独立分类,因此可以相应地传递每个8B。
特别是,如果没有更多的参数寄存器,则它们可能最终位于堆栈上。


谢谢你的出色回答,玛格丽特。我能否请你提及返回值和非平凡的复制构造函数/析构函数?然后我会很高兴将其标记为已接受,并且它可以成为一个非常好的参考,无需下载PDF文件。 :-) - jacobsa
@jacobsa 当然,我会尽快更新答案。 - Margaret Bloom
@jacobsa 更新了。我没有包含ABI用来分类类型的各种类的定义。在我看来,那将会太过宽泛。 - Margaret Bloom
{btsdaf} - Margaret Bloom
1
{btsdaf} - BeeOnRope
显示剩余4条评论

6

x86-64 ABI的文档在此处(链接),版本为252(截至本回答时是最新的ABI),可在此处(链接)下载。

如果我正确地阅读了第21页及其后面的内容,它说如果结构体的sizeof为8字节或更小,则会传递到普通寄存器中。之后的规则变得复杂,但我认为如果大小为9-16字节,则可能会传递到SSE寄存器中。

关于类,记住类和结构体之间唯一的区别是默认访问权限。但是,规则确实清楚地说明,如果存在非平凡的复制构造函数或非平凡的析构函数,则结构体将作为隐藏引用传递。


3
每个类都有一个复制构造函数。关键点是“非平凡的复制构造函数或析构函数”。这就是为什么= default;很重要,以及为什么unique_ptr不是零成本抽象的原因。 - Kerrek SB
@KerrekSB 很好的发现。 - Martin Bonner supports Monica
@KerrekSB:我本来想来告诉你关于unique_ptr的问题你错了,但是不,你完全正确。今天你让我大开眼界。 - jacobsa
@jacobsa: 我对那个 Godbolt 链接感到困惑。unique_ptr 版本似乎没有删除 unique ptr 持有的内存。 - Martin Bonner supports Monica

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接