GCC/Clang x86_64 С++ ABI несоответствие при возврате кортежа?

При попытке оптимизировать возвращаемые значения на x86_64 я заметил странную вещь. А именно, учитывая код:

#include <cstdint>
#include <tuple>
#include <utility>

using namespace std;

constexpr uint64_t a = 1u;
constexpr uint64_t b = 2u;

pair<uint64_t, uint64_t> f() { return {a, b}; }
tuple<uint64_t, uint64_t> g() { return tuple<uint64_t, uint64_t>{a, b}; }

Clang 3.8 выводит этот код сборки для f:

movl $1, %eax
movl $2, %edx
retq

а для g:

movl $2, %eax
movl $1, %edx
retq

которые выглядят оптимальными. Однако, когда скомпилирован с GCC 6.1, в то время как сгенерированная сборка для f идентична выпуску Clang, сборка, сгенерированная для g является:

movq %rdi, %rax
movq $2, (%rdi)
movq $1, 8(%rdi)
ret

Похоже, что тип возвращаемого значения классифицируется как MEMORY по GCC, но как INTEGER by Clang. Я могу подтвердить, что связывание кода Clang с кодом GCC, такой код может привести к ошибкам сегментации (Clang-вызов GCC-скомпилирован g(), который записывает туда, где имеет место %rdi) и возвращаемое недопустимое значение (вызов GCC Clang-compiled g()). Какой компилятор неисправен?

Связанный:

См. также

Ответ 1

Как показывает ответ davmac, libstdС++ std::tuple тривиально копируется конструктивно, но не тривиально перемещается конструктивно. Два компилятора не согласны с тем, должен ли конструктор перемещения влиять на соглашения о передаче аргументов.

Связанная с С++ нить ABI, похоже, объясняет это несогласие: http://sourcerytools.com/pipermail/cxx-abi-dev/2016-February/002891.html

В заключение, Clang реализует именно то, что говорит спецификация ABI, но g++ реализует то, что он должен был сказать, но не был обновлен, чтобы на самом деле сказать.

Ответ 2

ABI заявляет, что значения параметров классифицируются в соответствии с определенным алгоритмом. Релевантно здесь:

  1. Если размер агрегата превышает один восьмибайт, каждый из них классифицируется отдельно. Каждый восьмибайт инициализируется классом NO_CLASS.

  2. Каждое поле объекта классифицируется рекурсивно, так что всегда рассматриваются два поля. Полученный класс вычисляется в соответствии с классами полей в восьмерке:

В этом случае каждое из полей (для кортежа или пары) имеет тип uint64_t и поэтому занимает весь "восьмибайт". Таким образом, "два поля", которые должны учитываться в каждом восьмибайте, являются полями "NO_CLASS" (по 3) и uint64_t, которые классифицируются как INTEGER.

Также существует связь с передачей параметров:

Если объект С++ имеет либо нетривиальный конструктор копии, либо нетривиальный деструктор, он передается невидимой ссылкой (объект заменяется в списке параметров указателем с классом INTEGER)

Объект, который не отвечает этим требованиям, должен иметь адрес и, следовательно, должен быть в памяти, поэтому это требование существует. То же самое верно для возвращаемых значений, хотя это, по-видимому, опускается в спецификации (возможно, случайно).

Наконец, есть:

(c) Если размер агрегата превышает два восьмибайта, а первый восьмибайт не является SSE или любой другой восемьбайт не является SSEUP, весь аргумент передается в памяти.

Это не применимо здесь, очевидно; размер совокупности составляет ровно две восемь байт.

При возврате значений текст гласит:

  • Классифицировать тип возврата с помощью алгоритма классификации

Это означает, что, как указано выше, кортеж следует классифицировать как INTEGER. Тогда:

  1. Если класс INTEGER, следующий доступный регистр последовательности % rax,% rdx.

Это совершенно ясно.

Единственный еще открытый вопрос заключается в том, являются ли типы нетривиально-копируемыми-конструктивными/разрушаемыми. Как уже упоминалось выше, значения такого типа не могут быть переданы или возвращены в регистры, хотя спецификация, похоже, не распознает проблему для возвращаемых значений. Однако мы можем легко показать, что кортеж и пара являются тривиально-копируемыми и тривиально разрушаемыми, используя следующую программу:

Программа тестирования:

#include <utility>
#include <cstdint>
#include <tuple>
#include <iostream>

using namespace std;

int main(int argc, char **argv)
{
    cout << "pair is trivial? : " << is_trivial<pair<uint64_t, uint64_t> >::value << endl;
    cout << "pair is trivially_copy_constructible? : " << is_trivially_copy_constructible<pair<uint64_t, uint64_t> >::value << endl;
    cout << "pair is standard_layout? : " << is_standard_layout<pair<uint64_t, uint64_t> >::value << endl;
    cout << "pair is pod? : " << is_pod<pair<uint64_t, uint64_t> >::value << endl;
    cout << "pair is trivially_destructable? : " << is_trivially_destructible<pair<uint64_t, uint64_t> >::value << endl;
    cout << "pair is trivially_move_constructible? : " << is_trivially_move_constructible<pair<uint64_t, uint64_t> >::value << endl;

    cout << "tuple is trivial? : " << is_trivial<tuple<uint64_t, uint64_t> >::value << endl;
    cout << "tuple is trivially_copy_constructible? : " << is_trivially_copy_constructible<tuple<uint64_t, uint64_t> >::value << endl;
    cout << "tuple is standard_layout? : " << is_standard_layout<tuple<uint64_t, uint64_t> >::value << endl;
    cout << "tuple is pod? : " << is_pod<tuple<uint64_t, uint64_t> >::value << endl;
    cout << "tuple is trivially_destructable? : " << is_trivially_destructible<tuple<uint64_t, uint64_t> >::value << endl;
    cout << "tuple is trivially_move_constructible? : " << is_trivially_move_constructible<tuple<uint64_t, uint64_t> >::value << endl;
    return 0;
}

Вывод при компиляции с помощью GCC или Clang:

pair is trivial? : 0
pair is trivially_copy_constructible? : 1
pair is standard_layout? : 1
pair is pod? : 0
pair is trivially_destructable? : 1
pair is trivially_move_constructible? : 1
tuple is trivial? : 0
tuple is trivially_copy_constructible? : 1
tuple is standard_layout? : 0
tuple is pod? : 0
tuple is trivially_destructable? : 1
tuple is trivially_move_constructible? : 0

Это означает, что GCC ошибается. Возвращаемое значение должно быть передано в% rax,% rdx.

(Основные заметные различия между типами заключаются в том, что pair является стандартным макетом и тривиально конструктивен для перемещения, тогда как tuple не является, поэтому возможно, что GCC всегда возвращает нетривиально-перемещаемые значения через указатель, например).