Я смотрю выступление Чендлера Каррута в CppCon 2019:
Нет абстракций с нулевой стоимостью
в нем он приводит пример того, как он был удивлен тем, сколько накладных расходов вы понесли, используя std::unique_ptr<int>
овер int*
; этот сегмент начинается примерно в момент времени 17:25.
Вы можете взглянуть на результаты компиляции его примера пары фрагментов (godbolt.org) - чтобы убедиться, что, действительно, кажется, что компилятор не хочет передавать значение unique_ptr, которое фактически в нижней строке просто адрес - внутри регистра, только в прямой памяти.
Примерно в 27:00 г-н Каррут отмечает, что ABI C ++ требует, чтобы параметры-значения (некоторые, но не все; возможно, не примитивные типы? Нетривиально-конструируемые типы?) Передавались в памяти. а не в реестре.
Мои вопросы:
- Это требование ABI на некоторых платформах? (который?) Или, может быть, это просто пессимизация в определенных сценариях?
- Почему ABI такой? То есть, если поля структуры / класса вписываются в регистры или даже в один регистр - почему мы не можем передать его в этот регистр?
- Обсуждает ли этот комитет по стандартам C ++ этот вопрос в последние годы или когда-либо?
PS - чтобы не оставить этот вопрос без кода:
Обычный указатель:
void bar(int* ptr) noexcept;
void baz(int* ptr) noexcept;
void foo(int* ptr) noexcept {
if (*ptr > 42) {
bar(ptr);
*ptr = 42;
}
baz(ptr);
}
Уникальный указатель:
using std::unique_ptr;
void bar(int* ptr) noexcept;
void baz(unique_ptr<int> ptr) noexcept;
void foo(unique_ptr<int> ptr) noexcept {
if (*ptr > 42) {
bar(ptr.get());
*ptr = 42;
}
baz(std::move(ptr));
}
источник
this
указателе, который указывает на правильное местоположение.unique_ptr
есть те. Раскрытие регистра для этой цели сведет на нет всю оптимизацию «прохода в регистре».Ответы:
Одним из примеров является система V Application Binary Interface AMD64 Архитектура процессора Дополнение . Этот ABI предназначен для 64-битных x86-совместимых процессоров (архитектура Linux x86_64). Это сопровождается в Solaris, Linux, FreeBSD, macOS, Windows Подсистема для Linux:
Обратите внимание, что только 2 регистра общего назначения могут использоваться для передачи 1 объекта с тривиальным конструктором копирования и тривиальным деструктором, то есть только значения объектов с
sizeof
не более 16 могут быть переданы в регистрах. См. Соглашения о вызовах от Agner Fog для детальной обработки соглашений о вызовах, в частности §7.1 Передача и возврат объектов. Существуют отдельные соглашения о вызовах для передачи типов SIMD в регистрах.Существуют разные ABI для других архитектур ЦП.
Это деталь реализации, но когда обрабатывается исключение, при разматывании стека объекты с автоматической продолжительностью хранения должны быть адресуемыми относительно фрейма стека функций, поскольку к этому времени регистры были засорены. Коду раскрутки стека требуются адреса объектов для вызова их деструкторов, но у объектов в регистрах нет адреса.
Педантично, деструкторы действуют на объекты :
и объект не может существовать в C ++, если для него не выделено адресуемое хранилище, поскольку идентификатор объекта является его адресом .
Когда требуется адрес объекта с тривиальным конструктором копирования, который хранится в регистрах, компилятор может просто сохранить объект в памяти и получить адрес. Если конструктор копирования нетривиален, с другой стороны, компилятор не может просто сохранить его в памяти, ему скорее нужно вызвать конструктор копирования, который берет ссылку и, следовательно, требует адрес объекта в регистрах. Соглашение о вызовах, вероятно, не может зависеть от того, был ли конструктор копирования встроен в вызываемый объект или нет.
Еще один способ думать об этом заключается в том, что для тривиально копируемых типов компилятор передает значение объекта в регистрах, из которых объект может быть восстановлен обычными хранилищами памяти при необходимости. Например:
на x86_64 с System V ABI компилируется в:
В своем наводящем на размышления выступлении Чендлер Каррут упоминает, что может потребоваться переломное изменение ABI (среди прочего) для осуществления разрушительного движения, которое могло бы улучшить положение вещей. IMO, изменение ABI могло бы быть неразрывным, если функции, использующие новый ABI, явно соглашаются иметь новую другую связь, например объявлять их в
extern "C++20" {}
блоке (возможно, в новом встроенном пространстве имен для переноса существующих API). Так что только код, скомпилированный с новыми объявлениями функций с новой связью, может использовать новый ABI.Обратите внимание, что ABI не применяется, когда вызываемая функция была встроена. Как и при генерации кода во время компиляции, компилятор может встроить функции, определенные в других единицах перевода, или использовать пользовательские соглашения о вызовах.
источник
С обычными ABI нетривиальный деструктор -> не может пройти в регистрах
(Иллюстрация точки в ответе @ MaximEgorushkin с использованием примера @ harold в комментарии; исправлено согласно комментарию @ Yakk.)
Если вы компилируете:
ты получаешь:
т.е.
Foo
объект передаетсяtest
в register (edi
), а также возвращается в register (eax
).Когда деструктор не является тривиальным (как в
std::unique_ptr
примере с OP) - общие ABI требуют размещения в стеке. Это верно, даже если деструктор вообще не использует адрес объекта.Таким образом, даже в крайнем случае деструктора бездействия, если вы компилируете:
ты получаешь:
с бесполезной загрузкой и хранением.
источник
std::unique_ptr
в регистр, не будет соответствовать.register
Ключевое слово было предназначено , чтобы сделать его тривиальным для физической машины в магазине что - то в реестре, блокируя вещи , которые практически делают его труднее «не имеют никакого адреса» в физической машине.Если что-то видно на границе блока комплиментации, то независимо от того, определено оно явно или нет, оно становится частью ABI.
Основная проблема заключается в том, что регистры все время сохраняются и восстанавливаются при перемещении вниз и вверх по стеку вызовов. Так что не практично иметь ссылку или указатель на них.
Встраивание и вытекающие из него оптимизации - это хорошо, когда это происходит, но разработчик ABI не может рассчитывать на это. Они должны спроектировать ABI в худшем случае. Я не думаю, что программисты были бы очень довольны компилятором, в котором ABI менялся в зависимости от уровня оптимизации.
Тривиально копируемый тип может передаваться в регистрах, поскольку логическая операция копирования может быть разделена на две части. Параметры копируются в регистры, используемые вызывающей стороной для передачи параметров, а затем вызываемой стороной копируется в локальную переменную. Таким образом, имеет ли локальная переменная место в памяти или нет - это только вопрос вызываемого абонента.
Тип, в котором конструктор копирования или перемещения должен использоваться с другой стороны, не может разделить операцию копирования таким образом, поэтому он должен быть передан в память.
Я понятия не имею, рассматривали ли органы по стандартизации это.
Очевидным решением для меня было бы добавить надлежащие деструктивные ходы (а не текущий полпути «действительного, но в остальном неопределенного состояния») к языку, а затем ввести способ пометить тип как допускающий «тривиальные деструктивные ходы». «даже если это не позволяет тривиальные копии.
но такое решение БУДЕТ требовать взлома ABI существующего кода для реализации для существующих типов, что может принести немало сопротивления (хотя разрывы ABI в результате новых стандартных версий C ++ не беспрецедентны, например, изменения std :: string в C ++ 11 привел к разрыву ABI ..
источник
unique_ptr
иshared_ptr
семантику:shared_ptr<T>
позволяет вам предоставить ctor 1) ptr x для производного объекта U, который будет удален со статическим типом U w / выражениемdelete x;
(поэтому вам не нужен виртуальный dtor здесь) 2) или даже пользовательская функция очистки. Это означает, что состояние выполнения используется внутриshared_ptr
блока управления для кодирования этой информации. OTOHunique_ptr
не имеет такой функциональности и не кодирует поведение удаления в состоянии; единственный способ настроить очистку - это создать еще один экземпляр шаблона (другой тип класса).Сначала нам нужно вернуться к тому, что значит передавать по значению и по ссылке.
Для языков, таких как Java и SML, передача по значению является простой (и нет передачи по ссылке), так же, как копирование значения переменной, так как все переменные являются просто скалярами и имеют встроенную семантику копирования: они либо считаются арифметическими. введите C ++ или «ссылки» (указатели с другим именем и синтаксисом).
В Си у нас есть скалярные и пользовательские типы:
В C ++ пользовательские типы могут иметь пользовательскую семантику копирования, которая позволяет действительно «объектно-ориентированное» программирование с объектами, владеющими их ресурсами и операциями «глубокого копирования». В таком случае операция копирования на самом деле является вызовом функции, которая может почти выполнять произвольные операции.
Для структур C, скомпилированных как C ++, «копирование» по-прежнему определяется как вызов пользовательской операции копирования (либо конструктора, либо оператора присваивания), которая неявно генерируется компилятором. Это означает, что семантика программы общего подмножества C / C ++ отличается в C и C ++: в C копируется целый тип агрегата, в C ++ вызывается неявно сгенерированная функция копирования для копирования каждого члена; конечный результат заключается в том, что в любом случае каждый член копируется.
(Думаю, есть исключение, когда копируется структура внутри объединения.)
Таким образом, для типа класса единственный способ (вне объединенных копий) создать новый экземпляр - через конструктор (даже для тех, у кого есть тривиальные конструкторы, сгенерированные компилятором).
Вы не можете получить адрес rvalue через унарный оператор,
&
но это не значит, что объекта rvalue нет; и объект по определению имеет адрес ; и этот адрес даже представлен синтаксической конструкцией: объект типа класса может быть создан только конструктором, и он имеетthis
указатель; но для тривиальных типов пользовательского конструктора не существует, поэтому нет места для размещенияthis
до тех пор, пока копия не будет сконструирована и не будет названа.Для скалярного типа значение объекта - это значение объекта, чистое математическое значение, хранящееся в объекте.
Для типа класса единственным понятием значения объекта является другая копия объекта, которая может быть сделана только конструктором копирования, реальной функцией (хотя для тривиальных типов эта функция настолько тривиальна, иногда они могут быть создается без вызова конструктора). Это означает, что значение объекта является результатом изменения глобального состояния программы при выполнении . Это не доступ математически.
Так что передача по значению на самом деле не вещь: это передача по вызову конструктора копирования , что менее привлекательно. Предполагается, что конструктор копирования будет выполнять разумную операцию «копирования» в соответствии с надлежащей семантикой типа объекта с учетом его внутренних инвариантов (которые являются абстрактными пользовательскими свойствами, а не внутренними свойствами C ++).
Передача по значению объекта класса означает:
Обратите внимание, что проблема не имеет отношения к тому, является ли сама копия объектом с адресом: все параметры функции являются объектами и имеют адрес (на уровне семантики языка).
Вопрос заключается в следующем:
В случае тривиального типа класса вы все равно можете определить член-копию оригинала, поэтому вы можете определить чистое значение оригинала из-за тривиальности операций копирования (конструктор копирования и присваивание). С произвольными специальными пользовательскими функциями это не так: значение оригинала должно быть составной копией.
Объекты класса должны быть созданы вызывающей стороной; у конструктора формально есть
this
указатель, но формализм здесь не уместен: все объекты формально имеют адрес, но только те, которые фактически используют свой адрес не чисто локальным образом (в отличие от*&i = 1;
чисто локального использования адреса), должны иметь четкое определение адрес.Объект должен обязательно передаваться по адресу, если он должен иметь адрес в обеих этих двух отдельно скомпилированных функциях:
Здесь, даже если
something(address)
это чистая функция или макрос или что-то (напримерprintf("%p",arg)
), которое не может сохранить адрес или связаться с другим объектом, у нас есть требование передавать по адресу, потому что адрес должен быть четко определен для уникального объекта,int
который имеет уникальный идентичность.Мы не знаем, будет ли внешняя функция «чистой» с точки зрения адресов, переданных ей.
Здесь возможность реального использования адреса в нетривиальном конструкторе или деструкторе на стороне вызывающего абонента, вероятно, является причиной для выбора безопасного, упрощенного маршрута и присвоения объекту идентификатора в вызывающем устройстве и передачи его адреса, так как он делает убедитесь, что любое нетривиальное использование его адреса в конструкторе, после конструирования и в деструкторе непротиворечиво :
this
должно казаться одинаковым в течение существования объекта.Нетривиальный конструктор или деструктор, как и любая другая функция, может использовать
this
указатель таким образом, что требуется согласованность его значения, даже если некоторые объекты с нетривиальными вещами могут этого не делать:Обратите внимание, что в этом случае, несмотря на явное использование указателя (явный синтаксис
this->
), идентичность объекта не имеет значения: компилятор вполне может использовать побитовое копирование объекта, чтобы переместить его и выполнить «копирование». Это основано на уровне «чистоты» использованияthis
в специальных функциях-членах (адрес не экранирует).Но чистота не является атрибутом, доступным на уровне стандартного объявления (существуют расширения компилятора, которые добавляют описание чистоты при объявлении не встроенных функций), поэтому вы не можете определить ABI на основе чистоты кода, который может быть недоступен (код может или не может быть встроенным и доступным для анализа).
Чистота измеряется как «безусловно чистая» или «нечистая или неизвестная». Точка соприкосновения или верхняя граница семантики (фактически максимальная) или LCM (наименьшее общее кратное) "неизвестна". Так что ABI останавливается на неизвестности.
Резюме:
Возможная будущая работа:
Является ли аннотация чистоты достаточно полезной для обобщения и стандартизации?
источник
void foo(unique_ptr<int> ptr)
принимает объект класса по значению . Этот объект имеет член-указатель, но мы говорим о том, что сам объект класса передается по ссылке. (Поскольку это не просто копируемое копирование, поэтому его конструктор / деструктор должен быть непротиворечивымthis
.) Это реальный аргумент и не связан с первым примером передачи по ссылке явно ; в этом случае указатель передается в регистр.int
: я написал «умный fileno» пример, который показывает, что «владение» не имеет ничего общего с «переносом ptr».unique_ptr<T*>
это тот же размер и расположение, чтоT*
и в регистре. Тривиально копируемые объекты класса могут передаваться по значению в регистрах в x86-64 System V, как и большинство соглашений о вызовах. Это делает копию этогоunique_ptr
объекта, в отличие от вашегоint
примера , где вызываемая - х&i
является адрес вызывающей этоi
потому , что вы прошли по ссылке на уровне C ++ , а не только как деталь реализации ASM.unique_ptr
объекта; он использует,std::move
так что его можно безопасно скопировать, потому что это не приведет к 2 копиям одного и того жеunique_ptr
. Но для тривиально копируемого типа, да, он копирует весь агрегатный объект. Если это единственный член, хорошие соглашения о вызовах рассматривают его так же, как скаляр этого типа.struct{}
это структура C ++. Возможно, вам следует сказать «простые структуры» или «в отличие от С». Потому что да, есть разница. Если вы используетеatomic_int
в качестве члена структуры, C будет неатомно копировать его, ошибка C ++ в конструкторе удаленных копий. Я забыл, что делает C ++ для структур сvolatile
членами. C позволит вамstruct tmp = volatile_struct;
скопировать все это (полезно для SeqLock); С ++ не будет.