Как оптимизировать возвращаемые значения функции в C и С++ на x86-64?
x86-64 ABI определяет два возвращаемых регистра: rax
и rdx
, размер 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 };
}
даст лучшую производительность, учитывая текущее состояние компиляторов C/С++, ориентированных на x86-64? Есть ли какие-либо подводные камни по одной или другой версии? Компиляторы (GCC, Clang) всегда могут оптимизировать std::pair
для возврата в rax
и rdx
?
ОБНОВЛЕНИЕ: Как правило, возвращение пары происходит быстрее, если компилятор оптимизирует методы std::pair
(примеры двоичного вывода с GCC 5.3.0 и Clang 3.8.0). Если f()
не встроен, компилятор должен сгенерировать код для записи значения в память, например:
movq b, (%rdi)
movq a, %rax
retq
Но в случае g()
для компилятора достаточно:
movq a, %rax
movq b, %rdx
retq
Поскольку инструкции для записи значений в память обычно медленнее, чем инструкции для записи значений в регистры, вторая версия должна быть быстрее.
Ответы
Ответ 1
Так как ABI указывает, что в некоторых случаях для результата 2-го слова должны использоваться два регистра, любой соответствующий компилятор должен подчиняться этому правилу.
Однако для таких крошечных функций я предполагаю, что большая часть производительности будет получена от inlining.
Вы можете скомпилировать и связать с g++ -flto -O2
с помощью оптимизации времени соединения.
Я предполагаю, что вторая функция (возвращающая пару через 2 регистра) может быть немного быстрее и что, возможно, в некоторых ситуациях компилятор GCC может встроить и оптимизировать первый во второй.
Но вам действительно нужно ориентироваться, если вам это очень понравится.
Ответ 2
Обратите внимание, что ABI определяет упаковку любой небольшой структуры в регистры для передачи/возврата (если она содержит только целые типы). Это означает, что возврат a std::pair<uint32_t, uint32_t>
означает, что значения должны быть сдвинуты + ORed в rax
.
Это, вероятно, еще лучше, чем путешествие в оба конца через память, поскольку настройка пространства для указателя и передача этого указателя в качестве дополнительного аргумента имеет некоторые накладные расходы. (Помимо этого, однако, кругооборот через кеш L1 довольно дешев, например, задержка ~ 5 с. Магазин/загрузка почти наверняка попадет в кеш-память L1, поскольку память стека используется все время. Даже если он промахивается, сохранение хранилища еще может произойти, поэтому выполнение не останавливается до тех пор, пока ROB не заполнится, потому что магазин не сможет уйти в отставку. См. Руководство по микроаршруту Agner Fog и другие материалы в x86 теги wiki. )
В любом случае вот код, который вы получаете от gcc 5.3 -O2
, используя функции, которые принимают args вместо возврата константы времени компиляции значения (что приведет к 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
Так что это действительно неплохо. Два или три insns в вызывающем и вызываемом для упаковки и распаковки пары значений uint32_t
. Однако нигде не так хорошо, как возврат пары значений uint64_t
.
Если вы специально оптимизируете для x86-64 и заботитесь о том, что происходит для не-встроенных функций с несколькими возвращаемыми значениями, то предпочитает возвращать std::pair<uint64_t, uint64_t>
(или int64_t
, очевидно), даже если вы назначаете эти пары более узким целым в вызывающем. Обратите внимание, что в x32 ABI (-mx32
) указатели имеют всего 32 бита. Не предполагайте, что указатели имеют 64 бит при оптимизации для x86-64, если вы заботитесь об этом ABI.
Если какой-либо из членов пары имеет 64 бит, они используют отдельные регистры. Он не делает ничего глупого, как разделение одного значения между верхней половиной одной рег и нижней половиной другой.