【问题标题】:How to optimize function return values in C and C++ on x86-64?如何在 x86-64 上优化 C 和 C++ 中的函数返回值?
【发布时间】:2014-10-12 10:54:21
【问题描述】:

x86-64 ABI 指定两个返回寄存器:raxrdx,大小均为 64 位(8 字节)。

假设 x86-64 是唯一的目标平台,这两个功能中的哪一个:

uint64_t f(uint64_t * const secondReturnValue) {
    /* Calculate a and b. */
    *secondReturnValue = b;
    return a;
}

std::pair<uint64_t, uint64_t> g() {
    /* Calculate a and b, same as in f() above. */
    return { a, b };
}

考虑到针对 x86-64 的 C/C++ 编译器的当前状态,会产生更好的性能吗?使用一个或另一个版本在性能方面是否存在任何缺陷?编译器(GCC、Clang)是否总是能够优化 std::pair 以在 raxrdx 中返回?

更新: 通常,如果编译器优化了 std::pair 方法(GCC 5.3.0Clang 3.8.0 的二进制输出示例),则返回一对会更快。如果f() 没有内联,编译器必须生成代码来将值写入内存,例如:

movq b, (%rdi)
movq a, %rax
retq

但如果是g(),编译器就足够了:

movq a, %rax
movq b, %rdx
retq

因为将值写入内存的指令通常比将值写入寄存器的指令慢,所以第二个版本应该更快。

【问题讨论】:

  • 您是否尝试测量性能?
  • 真的需要这么小的性能吗?
  • @Sathish 这是有效的语法。相当于return std::pair&lt;uint64_t, uint64_t&gt;{a, b};
  • @Sathish:您可以返回一个包含 2 个数字的结构。 x86-64 ABI 表示,在特定情况下,它会在 2 个寄存器中返回。
  • 您是否尝试过查看发出的程序集?您是在寻找仅 Unix 的答案(给定编译器)还是对 MSVC 的答案也感兴趣? (它使用不同的 ABI)

标签: c++ c performance x86-64 abi


【解决方案1】:

由于 ABI 规定在某些特定情况下必须使用两个寄存器来生成 2 字结果,因此任何符合标准的编译器都必须遵守该规则。

但是,对于这么小的函数,我猜大部分性能将来自内联。

您可能希望使用链接时优化编译和链接g++ -flto -O2

我猜第二个函数(通过 2 个寄存器返回一对)可能会稍微快一些,而且在某些情况下,GCC 编译器可能会将第一个函数内联并优化到第二个函数中。

但如果你那么在意的话,你真的应该进行基准测试。

【讨论】:

    【解决方案2】:

    请注意,ABI 指定将任何小型结构打包到寄存器中以用于传递/返回(如果它仅包含整数类型)。这意味着返回 std::pair&lt;uint32_t, uint32_t&gt; 意味着必须将值移位 + 或运算到 rax

    这可能仍然比通过内存往返要好,因为为指针设置空间并将该指针作为额外的 arg 传递会产生一些开销。 (不过,除此之外,通过 L1 缓存的往返非常便宜,例如约 5c 延迟。存储/加载几乎肯定会在 L1 缓存中命中,因为一直在使用堆栈内存。即使它错过了, 存储转发仍然会发生,因此执行不会停止,直到 ROB 填满,因为存储无法退出。请参阅 Agner Fog's microarch guide 标签 wiki 上的其他内容。)

    无论如何,here's the kind of code you get from gcc 5.3 -O2,使用带参数的函数而不是返回编译时常量值(这将导致movabs rax, 0x...):

    #include <cstdint>
    #include <utility>
    #define type_t uint32_t
    
    type_t f(type_t * const secondReturnValue, type_t x) {
        *secondReturnValue = x+4;
        return x+2;
    }
        lea     eax, [rsi+4]           # LEA is an add-and-shift instruction that uses memory-operand syntax and encoding
        mov     DWORD PTR [rdi], eax
        lea     eax, [rsi+2]
        ret
    
    std::pair<type_t, type_t> g(type_t x) { return {x+2, x+4}; }
        lea     eax, [rdi+4]
        lea     edx, [rdi+2]
        sal     rax, 32
        or      rax, rdx
        ret
    
    type_t use_pair(std::pair<type_t, type_t> pair) {
        return pair.second + pair.first;
    }
        mov     rax, rdi
        shr     rax, 32
        add     eax, edi
        ret
    

    所以这真的一点也不差。调用者和被调用者中的两个或三个insn 来打包和解包一对uint32_t 值。不过,还不如返回一对 uint64_t 值。

    如果您专门针对 x86-64 进行优化,并且关心具有多个返回值的非内联函数会发生什么,那么更喜欢返回 std::pair&lt;uint64_t, uint64_t&gt;(或 int64_t,显然),即使您将这些对分配给调用者中较窄的整数。请注意,在 x32 ABI (-mx32) 中,指针只有 32 位。如果您关心 ABI,则在针对 x86-64 进行优化时不要假设指针是 64 位的。

    如果这对中的任何一个成员是 64 位,则它们使用单​​独的寄存器。它不会做任何愚蠢的事情,比如在一个 reg 的高半部分和另一个 reg 的低半部分之间分割一个值。

    【讨论】:

      猜你喜欢
      • 2021-09-27
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2013-06-16
      • 2019-09-19
      • 1970-01-01
      • 2017-07-13
      相关资源
      最近更新 更多