How to optimize function return values in C and C++ on x86-64?

一笑奈何 提交于 2019-11-29 06:11:06

Since the ABI specifies that in some particular cases two registers have to be used for the 2-word result any conforming compiler has to obey that rule.

However, for such tiny functions I guess that most of the performance will come from inlining.

You may want to compile and link with g++ -flto -O2 using link-time optimizations.

I guess that the second function (returning a pair thru 2 registers) might be slightly faster, and that perhaps in some situations the GCC compiler could inline and optimize the first into the second.

But you really should benchmark if you care that much.

Note that the ABI specifies packing any small struct into registers for passing/returning (if it contains only integer types). This means that returning a std::pair<uint32_t, uint32_t> means the values have to be shift+ORed into rax.

This is probably still better than a round trip through memory, because setting up space for a pointer, and passing that pointer as an extra arg, has some overhead. (Other than that, though, a round-trip through L1 cache is pretty cheap, like ~5c latency. The store/load are almost certainly going to hit in L1 cache, because stack memory is used all the time. Even if it misses, store-forwarding can still happen, so execution doesn't stall until the ROB fills because the store can't retire. See Agner Fog's microarch guide and other stuff at the tag wiki.)

Anyway, here's the kind of code you get from gcc 5.3 -O2, using functions that take args instead of returning compile-time constant values (which would lead to 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

So it's really not bad at all. Two or three insns in the caller and callee to pack and unpack a pair of uint32_t values. Nowhere near as good as returning a pair of uint64_t values, though.

If you're specifically optimizing for x86-64, and care what happens for non-inlined functions with multiple return values, then prefer returning std::pair<uint64_t, uint64_t> (or int64_t, obviously), even if you assign those pairs to narrower integers in the caller. Note that in the x32 ABI (-mx32), pointers are only 32bits. Don't assume pointers are 64bit when optimizing for x86-64, if you care about that ABI.

If either member of the pair is 64bit, they use separate registers. It doesn't do anything stupid like splitting one value between the high half of one reg and the low half of another.

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!