Ответ 1
Краткий ответ заключается в том, что libstc++
стандартной библиотеки libstc++
используемая gcc
и clang
в Linux, реализует std::tuple
с нетривиальным конструктором перемещения (в частности, базовый класс _Tuple_impl
имеет нетривиальный конструктор перемещения). С другой стороны, конструкторы копирования и перемещения для std::pair
все по умолчанию.
Это, в свою очередь, вызывает связанную с C++ -ABI разницу в соглашении о вызовах для возврата этих объектов из функций, а также для передачи их по значению.
Гори Детали
Вы провели свои тесты на Linux, который придерживается SysV x86-64 ABI. В этом ABI есть специальные правила для передачи или возврата классов или структур в функции, о которых вы можете прочитать здесь. Особый случай нас интересует, получат ли два поля int
в этих структурах класс INTEGER
или класс MEMORY
.
Недавняя версия спецификации ABI имеет следующее:
Классификация агрегатных (структур и массивов) и объединяющих типов работает следующим образом:
- Если размер объекта больше восьми восьмибайтов или содержит выровненные поля un-, он имеет класс MEMORY 12.
- Если объект C++ имеет нетривиальный конструктор копирования или нетривиальный деструктор 13, он передается по невидимой ссылке (объект заменяется в списке параметров указателем с классом INTEGER) 14.
- Если размер совокупности превышает один восьмибайтовый, каждый классифицируется отдельно. Каждый восьмибайт инициализируется в классе NO_CLASS.
- Каждое поле объекта классифицируется рекурсивно, поэтому всегда учитываются два поля. Полученный класс рассчитывается по классам полей в восьмибайтовом
Здесь применимо условие (2). Обратите внимание, что в нем упоминаются только конструкторы копирования, а не конструкторы перемещения, но вполне очевидно, что это просто дефект в спецификации, учитывая введение конструкторов перемещения, которые обычно должны быть включены в любой алгоритм классификации, где конструкторы копирования были включены ранее., В частности, IA-64 cxx-abi, о котором gcc
задокументировано , включает конструкторы перемещения:
Если тип параметра нетривиален для целей вызовов, вызывающая сторона должна выделить место для временного объекта и передать это временное значение по ссылке. В частности:
- Пространство выделяется вызывающей стороной обычным образом для временного, обычно в стеке.
и тогда определение нетривиально:
Тип считается нетривиальным для вызовов, если:
- у него есть нетривиальный конструктор копирования, конструктор перемещения или деструктор, или
- все его конструкторы копирования и перемещения удаляются.
Таким образом, поскольку tuple
не считается тривиально копируемым с точки зрения ABI, он получает обработку MEMORY
, что означает, что ваша функция должна заполнять выделенный объект стека, передаваемый вызываемым в rdi
. Функция std::pair
может просто передавать всю структуру в rax
поскольку она помещается в один EIGHTBYTE
и имеет класс INTEGER
.
Это имеет значение? Да, строго говоря, автономная функция, подобная той, которую вы скомпилировали, будет менее эффективной для tuple
так как эта ABI-версия "запекается".
Однако часто компилятор сможет увидеть тело функции и встроить его или выполнить межпроцедурный анализ, даже если он не встроен. В обоих случаях ABI больше не важен, и, вероятно, оба подхода будут одинаково эффективными, по крайней мере, с приличным оптимизатором. Например, давайте вызовем ваши функции f1()
и f2()
и немного посчитаем результат:
int add_pair() {
auto p = f1();
return p.first + p.second;
}
int add_tuple() {
auto t = f2();
return std::get<0>(t) + std::get<1>(t);
}
В принципе, метод add_tuple
начинается с недостатка, так как он должен вызывать f2()
который менее эффективен, и он также должен создать временный объект кортежа в стеке, чтобы он мог передать его в f2
как скрытый параметр. Ну, не важно, обе функции полностью оптимизированы, чтобы просто возвращать правильное значение напрямую:
add_pair():
mov eax, 819
ret
add_tuple():
mov eax, 819
ret
Таким образом, в целом вы можете сказать, что эффект этой проблемы ABI с tuple
будет относительно приглушенным: он добавляет небольшие фиксированные накладные расходы к функциям, которые должны соответствовать ABI, но это действительно будет иметь значение только в относительном смысле для очень маленьких функций - но такие функции могут быть объявлены в месте, где они могут быть встроены (или, если нет, вы оставляете производительность на столе).
libcst C++ против lib C++ +
Как объяснено выше, это проблема ABI, а не проблема оптимизации, как таковая. И clang, и gcc уже оптимизируют код библиотеки в максимально возможной степени в соответствии с ограничениями ABI - если они сгенерируют код, подобный f1()
для случая std::tuple
они нарушат ABI-совместимые вызывающие объекты.
Это ясно видно, если вы переключитесь на использование libC++
а не Linux по умолчанию для libstdC++
- эта реализация не имеет явного конструктора перемещения (как упоминает Марк Глисс в комментариях, они застряли в этой реализации). для обратной совместимости). Теперь clang
(и, вероятно, gcc, хотя я не пробовал), генерирует одинаковый оптимальный код в обоих случаях:
f1(): # @f1()
movabs rax, 2345052143889
ret
f2(): # @f2()
movabs rax, 2345052143889
ret
Ранние версии Clang
Почему версии clang
компилируют это по-другому? Это была просто ошибка в clang или ошибка в спецификации, в зависимости от того, как вы на нее смотрите. Спецификация явно не включает конструкцию перемещения в случаях, когда необходимо передать скрытый указатель на временный объект. не соответствовал IA-64 C++ ABI. Например, скомпилированный способ, которым clang использовал это, был несовместим с gcc
или более новыми версиями clang
. Спецификация была в конечном итоге обновлена, и поведение clang изменилось в версии 5.0.
Обновление: Марк Глисс упоминает в комментариях, что изначально была путаница во взаимодействии нетривиальных конструкторов движений и C++ ABI, и clang
изменил свое поведение в какой-то момент, что, вероятно, объясняет переключение:
Спецификация ABI для некоторых случаев передачи аргументов с участием конструкторов перемещения была неясной, и когда они были прояснены, clang изменился, чтобы следовать ABI. Это, наверное, один из тех случаев.