x86-64 上的 C++:structs/classes 何时在寄存器中传递和返回?

C++ on x86-64: when are structs/classes passed and returned in registers?

假设 Linux 上的 x86-64 ABI,在 C++ 中的什么条件下,结构传递给函数是在寄存器中还是在堆栈中?在什么情况下它们会返回到寄存器中? 类 的答案有变化吗?

如果这有助于简化答案,您可以假设一个 argument/return 值并且没有浮点值。

记录了 x86-64 ABI here with version 252 (the latest ABI as of my answer) downloadable here

如果我正确阅读了第 21 页及以后的内容,它说如果 sizeof(struct) 是 8 个字节或更少,那么它将被传递到一个普通的寄存器中。之后规则变得复杂,但我认为如果它是 9-16 字节,它可能会在 SSE 寄存器中通过。

至于 classes,请记住 class 和结构之间的唯一区别是默认访问。 但是规则明确指出,如果存在非平凡的复制构造函数或非平凡的析构函数,该结构将作为隐藏引用传递。

ABI 规范已定义 here
更新版本可用 here

我假设 reader 已经习惯了文档的术语,并且他们可以 class 化基本类型。


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

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]
}

Copy elision 在这里工作

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

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]
}

如果上述none为真,则考虑对象的字段。
如果其中一个字段本身是 struct/class,则递归应用该过程。
目标是 class 确定对象中两个八字节 (8B) 中的每一个。

每个8B的字段class都被考虑了。
请注意,由于上述对齐要求,整数个字段总是完全占用一个 8B。

设置C为8B的class,D为字段的class考虑 class.
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的class计算如下

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 的 class,比如说 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;
}

8B 及其领域

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

a是INTEGER,所以第一个8B是INTEGER。 b 是 X87 和 X87UP 所以第二个 8B 是 MEMORY。 最后的 class 是两个 8B 的 MEMORY。


例子

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

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

8B 及其领域

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

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

最后的 class 是计算出来的。


Return 值

值相应地返回到它们的 classes:

  • 内存
    调用者将一个隐藏的第一个参数传递给函数,以便将结果存储到其中。
    在 C++ 中,这通常涉及复制 elision/return 值优化。 此地址必须返回到 eax,从而将 MEMORY classes "by reference" 返回到隐藏的调用者分配的缓冲区。

    If the type has class MEMORY, then the caller provides space for the return value and passes the address of this storage in %rdi as if it were the first argument to the function. In effect, this address becomes a “hidden” first argument. On return %rax will contain the address that has been passed in by the caller in %rdi.

  • INTEGERPOINTER
    根据需要注册raxrdx

  • SSESSEUP 根据需要注册 xmm0xmm1

  • X87X87UP 寄存器st0


PODs

技术定义是here

ABI 的定义报告如下。

A de/constructor is trivial if it is an implicitly-declared default de/constructor and if:

   • its class has no virtual functions and no virtual base classes, and
   • all the direct base classes of its class have trivial de/constructors, and
   • for all the nonstatic data members of its class that are of class type (or array thereof), each such class has a trivial de/constructor.


请注意,每个 8B 都是 class 独立化的,因此每个 8B 都可以相应地通过。
特别是,如果没有更多的参数寄存器,它们可能最终会进入堆栈。