关于在C/ c++ /汇编中返回多个值

About returning more than one value in C/C++/Assembly

本文关键字:返回 c++ 汇编      更新时间:2023-10-16

我读过一些关于返回多个值的问题,例如在c++和Java中只有一个返回值背后的原因是什么?、从c++函数返回多个值以及为什么大多数编程语言只支持从函数返回单个值?

我同意大多数用来证明多个返回值不是严格必要的参数,我理解为什么这样的功能还没有实现,但我仍然不能理解为什么我们不能使用多个调用者保存的寄存器,如ECX和EDX来返回这样的值。

使用寄存器而不是创建一个类/结构来存储这些值或通过引用/指针传递参数,这两种方法都使用内存来存储它们,不是更快吗?如果可以这样做,是否有C/c++编译器使用此功能来加速代码?

<标题>编辑:

理想的代码应该是这样的:

(int, int) getTwoValues(void) { return 1, 2; }
int main(int argc, char** argv)
{
    // a and b are actually returned in registers
    // so future operations with a and b are faster
    (int a, int b) = getTwoValues();
    // do something with a and b
    
    return 0;
}

是的,有时会这样做。如果你读过维基百科关于cdecl下x86调用约定的页面:

对cdecl的解释有一些不同,特别是在如何返回值方面。因此,为不同的操作系统平台和/或由不同的编译器编译的x86程序可能是不兼容的,即使它们都使用"cdecl"约定并且不调用底层环境。一些编译器返回寄存器对EAX:EDX中长度为2或更少的简单数据结构,而需要由异常处理程序进行特殊处理的较大结构和类对象(例如,定义的构造函数、析构函数或赋值)则返回到内存中。要传递"in memory",调用者分配内存并传递指向它的指针作为隐藏的第一个参数;被调用方填充内存并返回指针,返回时弹出隐藏指针。

<一口>(强调我的)

最终,它归结为调用约定。您的编译器可以优化代码以使用它想要的任何寄存器,但是当您的代码与其他代码(如操作系统)交互时,它需要遵循标准调用约定,通常使用1个寄存器返回值。

在堆栈中返回并不一定较慢,因为一旦值在L1缓存中可用(堆栈通常会满足),访问它们将非常快。

然而,在大多数计算机体系结构中,至少有 2个寄存器来返回值,这些值的宽度是字长的两倍(或更多)(x86中的edx:eax, x86_64中的rdx:rax, MIPS中的$v0$v1(为什么MIPS汇编器有多个返回值寄存器?),ARM中的R0:R3 1, ARM64中的X0:X7…)。那些没有的大多是只有一个累加器或寄存器数量非常有限的微控制器。

1 "如果返回值的类型太大,无法容纳r0到r3,或者其大小在编译时无法静态确定,则调用者必须在运行时为该值分配空间,并在r0中传递指向该空间的指针。"

这些寄存器也可以用于直接返回适合2个(或更多,取决于体系结构和ABI)寄存器或更少的小结构体。

例如使用以下代码

struct Point
{
    int x, y;
};
struct shortPoint
{
    short x, y;
};
struct Point3D
{
    int x, y, z;
};
Point P1()
{
    Point p;
    p.x = 1;
    p.y = 2;
    return p;
}
Point P2()
{
    Point p;
    p.x = 1;
    p.y = 0;
    return p;
}
shortPoint P3()
{
    shortPoint p;
    p.x = 1;
    p.y = 0;
    return p;
}
Point3D P4()
{
    Point3D p;
    p.x = 1;
    p.y = 2;
    p.z = 3;
    return p;
}

Clang为x86_64发出以下指令,您可以在这里看到

P1():                                 # @P1()
    movabs  rax, 8589934593
    ret
P2():                                 # @P2()
    mov eax, 1
    ret
P3():                                 # @P3()
    mov eax, 1
    ret
P4():                                 # @P4()
    movabs  rax, 8589934593
    mov edx, 3
    ret
ARM64

:

P1():
    mov x0, 1
    orr x0, x0, 8589934592
    ret
P2():
    mov x0, 1
    ret
P3():
    mov w0, 1
    ret
P4():
    mov x1, 1
    mov x0, 0
    sub sp, sp, #16
    bfi x0, x1, 0, 32
    mov x1, 2
    bfi x0, x1, 32, 32
    add sp, sp, 16
    mov x1, 3
    ret
如您所见,没有涉及堆栈操作。您可以切换到其他编译器,以查看值主要在寄存器上返回。

返回的数据被放到堆栈中。通过复制返回结构体实际上与返回多个值是一样的,因为它的所有数据成员都放在堆栈上。如果需要多个返回值,这是最简单的方法。我知道在Lua中它就是这样处理它的,只是把它包装在一个struct中。为什么它从来没有实现过,可能是因为你可以用一个结构体来实现它,为什么要实现一个不同的方法呢?至于c++,它实际上支持多个返回值,但它是以特殊类的形式出现的,实际上与Java处理多个返回值(元组)的方式相同。所以最后,都是一样的,要么复制原始数据(非指针/非引用到结构体/对象),要么只是复制一个指针到存储多个值的集合。