【问题标题】:About returning more than one value in C/C++/Assembly关于在 C/C++/Assembly 中返回多个值
【发布时间】:2015-10-08 10:24:43
【问题描述】:

我已经阅读了一些关于返回多个值的问题,例如 What is the reason behind having only one return value in C++ and Java?Returning multiple values from a C++ functionWhy do most programming languages only support returning a single value from a function?

我同意大多数用于证明多个返回值并非绝对必要的论点,并且我理解为什么尚未实现此类功能,但我仍然不明白为什么我们不能使用多个调用者- 保存的寄存器(例如 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;
}

【问题讨论】:

  • 逻辑上你可以return 多个值,如果你的return 一个struct
  • 但是如果我返回一个结构,我不会返回一个指向内存中该结构的指针,该指针将用于在函数返回后获取值吗?
  • 好吧,fastcall 使用寄存器作为 ecx 和 edx 中的前 2 个参数并返回 eax,它并不比默认的 cdecl 调用约定快多少。一些基准测试甚至显示 fastcall 更慢。
  • 您是否有示例代码会生成比您喜欢的更差的机器代码(在优化的构建中)?
  • @user2565020 这是一个由编译器决定的实现细节

标签: c++ assembly optimization x86 return-value


【解决方案1】:

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

【讨论】:

  • 但是在堆栈上返回值实际上比在 EAX 和 ECX 上返回两个 int 值要慢。
  • Lua 不会将多个返回值包装在一个“结构”中;它有一个堆栈并将所有返回值推送到该堆栈上。您是否将return a, b, creturn {a, b, c} 混淆了?
  • 公平点,但我们在争论语义。如果它是一个结构,它会是一样的。所有值都以相同的方式压入堆栈。至于通过寄存器返回,您正在为那微不足道的优化而烦恼。是的,寄存器更快,但如果您实际检查时钟周期数并进行比较,然后转换为实时,我们谈论的是节省了无法估量的时间。编辑:我刚刚查了一下,推送操作与移动到寄存器的时钟周期几乎相同,所以真的没有任何区别。
  • 我检查了 4 个不同的处理器顺便说一句,差异大约是 1-2 个时钟周期,平均(基于相同的时钟速度)大约节省 0.0000000001 秒的时间(我希望我在那里放了足够的 0) .
【解决方案2】:

是的,有时会这样做。如果您阅读 cdecl 下 x86 calling conventions 上的 Wikipedia 页面:

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

(强调我的)

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

【讨论】:

  • 如果我理解正确,结构是否转换为适合 2 个寄存器?如果是这样,是否可以使用超过 2 个寄存器?
  • 数据结构需要适应 2 个寄存器。如果 2 个寄存器没有提供足够的位来存储整个结构,那么它需要使用内存来传递结构。但这是针对这个特定的调用约定的。编译器可以使用它想要的任意数量的寄存器(在物理限制内)来返回值,因此另一个编译器可以使用 3+ 个寄存器来返回值。
  • 有没有编译器会做这样的事情(超过 2 个寄存器),尤其是 MSVC 和 GCC?
  • 老实说,我不确定。但我也不确定这是否很重要。现代 CPU 有数百个寄存器,并在其管道中使用寄存器重命名和值转发。这类事情会让这样的优化变得不那么值得。
  • 64 位具有相同的寄存器,但改为 64 位,另外增加了大约两倍的专用寄存器(如 SSE 和 MMX 使用的那些),并增加了更多的 fpu 寄存器。由于更宽的总线地址,64 位还可以访问更多内存。虽然在大多数情况下,它并不是那么特别。如果你喜欢,技术。更多 CPU 的规格是公开可用的,如果您真的想了解它们的工作原理,这是一本很好的读物。它们包括完整的指令集、流水线解释、示例代码,以及制作操作系统所需的一切:)
【解决方案3】:

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

然而,在大多数计算机架构中,至少有 2 个寄存器来返回是字长两倍(或更多)宽度的值(x86 中为edx:eax , rdx:rax in x86_64, $v0 and $v1 in MIPS (Why MIPS assembler has more that one register for return value?), R0:R3 in ARM1, X0:X7 in ARM64 ...)。没有的大多是只有一个累加器或寄存器数量非常有限的微控制器。

1"If the type of value returned is too large to fit in r0 to r3, or whose size cannot be determined statically at compile time, then the caller must allocate space for that value at run time, and pass a pointer to that space in 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 发出以下指令,如您所见 here

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

如您所见,不涉及堆栈操作。可以切换到其他编译器看看,值主要是在寄存器上返回的。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2017-09-02
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2017-07-12
    • 2016-01-18
    • 2015-06-26
    相关资源
    最近更新 更多