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 заявляет, что значения параметров классифицируются в соответствии с определенным алгоритмом. Релевантно здесь:
-
Если размер агрегата превышает один восьмибайт, каждый из них классифицируется отдельно. Каждый восьмибайт инициализируется классом NO_CLASS.
-
Каждое поле объекта классифицируется рекурсивно, так что всегда рассматриваются два поля. Полученный класс вычисляется в соответствии с классами полей в восьмерке:
В этом случае каждое из полей (для кортежа или пары) имеет тип uint64_t
и поэтому занимает весь "восьмибайт". Таким образом, "два поля", которые должны учитываться в каждом восьмибайте, являются полями "NO_CLASS" (по 3) и uint64_t
, которые классифицируются как INTEGER.
Также существует связь с передачей параметров:
Если объект С++ имеет либо нетривиальный конструктор копии, либо нетривиальный деструктор, он передается невидимой ссылкой (объект заменяется в списке параметров указателем с классом INTEGER)
Объект, который не отвечает этим требованиям, должен иметь адрес и, следовательно, должен быть в памяти, поэтому это требование существует. То же самое верно для возвращаемых значений, хотя это, по-видимому, опускается в спецификации (возможно, случайно).
Наконец, есть:
(c) Если размер агрегата превышает два восьмибайта, а первый восьмибайт не является SSE или любой другой восемьбайт не является SSEUP, весь аргумент передается в памяти.
Это не применимо здесь, очевидно; размер совокупности составляет ровно две восемь байт.
При возврате значений текст гласит:
- Классифицировать тип возврата с помощью алгоритма классификации
Это означает, что, как указано выше, кортеж следует классифицировать как INTEGER. Тогда:
- Если класс 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 всегда возвращает нетривиально-перемещаемые значения через указатель, например).