Когда я должен передать или вернуть структуру по значению?
Структура может быть передана / возвращена по значению или передана / возвращена по ссылке (через указатель) в C.
По общему мнению, первое может быть применено к небольшим структурам без наказания в большинстве случаев. См. Есть ли случаи, когда возврат структуры напрямую является хорошей практикой? и есть ли какие-либо недостатки в передаче структур по значению в C, а не в передаче указателя?
И то, что избегание разыменования может быть полезным как с точки зрения скорости, так и с точки зрения ясности. Но что считается маленьким? Я думаю, что мы все можем согласиться, что это небольшая структура:
struct Point { int x, y; };
Что мы можем передать по значению с относительной безнаказанностью:
struct Point sum(struct Point a, struct Point b) {
return struct Point { .x = a.x + b.x, .y = a.y + b.y };
}
И это Linux task_struct
большая структура:
Мы хотели бы избежать размещения стека любой ценой (особенно с этими стеками режима ядра 8K!). Но как насчет средних? Я предполагаю, что структуры меньше, чем регистр в порядке. Но как насчет этого?
typedef struct _mx_node_t mx_node_t;
typedef struct _mx_edge_t mx_edge_t;
struct _mx_edge_t {
char symbol;
size_t next;
};
struct _mx_node_t {
size_t id;
mx_edge_t edge[2];
int action;
};
Каково лучшее эмпирическое правило для определения того, является ли структура достаточно маленькой, чтобы ее можно было безопасно передавать по значению (за исключением смягчающих обстоятельств, таких как некоторая глубокая рекурсия)?
Наконец, пожалуйста, не говорите мне, что мне нужно профилировать. Я прошу эвристику, чтобы использовать, когда мне лень / это не стоит того, чтобы исследовать дальше.
РЕДАКТИРОВАТЬ: у меня есть два вопроса на основе ответов на данный момент:
Что если структура на самом деле меньше указателя на нее?
Что, если мелкое копирование - это желаемое поведение (вызываемая функция все равно будет выполнять поверхностное копирование)?
РЕДАКТИРОВАТЬ: Не уверен, почему это было отмечено как возможный дубликат, так как я на самом деле связать другой вопрос в моем вопросе. Я прошу уточнить, что представляет собой небольшая структура, и хорошо знаю, что большую часть времени структуры должны передаваться по ссылке.
9 ответов
На небольших встроенных архитектурах (8/16 бит) - всегда проходить по указателю, так как нетривиальные структуры не вписываются в такие крошечные регистры, и эти машины, как правило, также испытывают недостаток в регистре.
На ПК-подобных архитектурах (32- и 64-битные процессоры) - передача структуры по значению в порядке sizeof(mystruct_t) <= 2*sizeof(mystruct_t*)
и функция не имеет много (обычно более 3 машинных слов) других аргументов. При этих обстоятельствах типичный оптимизирующий компилятор передает / возвращает структуру в регистре или регистровой паре. Тем не менее, в x86-32 этот совет следует принимать с изрядной долей соли, поскольку из-за чрезвычайного давления в регистре, с которым приходится сталкиваться компилятору x86-32, передача указателя может все же происходить быстрее из-за уменьшения различий и заполнения регистров.
Возврат структуры по значению в PC-лайках, с другой стороны, следует тому же правилу, за исключением того факта, что когда структура возвращается по указателю, структура, которую необходимо заполнить, должна также передаваться по указателю - в противном случае вызываемый и вызывающий абоненты застряли в необходимости договориться о том, как управлять памятью для этой структуры.
Мой опыт, почти 40 лет встраивания в реальном времени, последние 20 лет с использованием C; в том, что лучший способ - передать указатель.
В любом случае необходимо загрузить адрес структуры, а затем рассчитать смещение для области интересов...
При передаче всей структуры, если она не передана по ссылке, то
- он не помещается в стек
- это обычно копируется скрытым вызовом memcpy()
- он копируется в раздел памяти, который теперь "зарезервирован" и недоступен для любой другой части программы.
Аналогичные соображения существуют для случая, когда структура возвращается по значению.
Однако в этих регистрах передаются "небольшие" структуры, которые можно полностью сохранить в рабочем регистре до двух, особенно если в операторе компиляции используются определенные уровни оптимизации.
Детали того, что считается "маленьким", зависят от компилятора и базовой аппаратной архитектуры.
Поскольку часть вопроса с передачей аргументов уже получена, я сосредоточусь на возвращающейся части.
Лучшее, что можно сделать в IMO, это вообще не возвращать структуры или указатели на структуры, а передавать указатель на "структуру результата" функции.
void sum(struct Point* result, struct Point* a, struct Point* b);
Это имеет следующие преимущества:
result
struct может жить либо в стеке, либо в куче, по усмотрению вызывающей стороны.- Нет проблем с владением, так как ясно, что вызывающая сторона отвечает за распределение и освобождение структуры результата.
- Структура может быть даже длиннее, чем нужно, или может быть встроена в большую структуру.
То, как структура передается в функцию или из функции, зависит от двоичного интерфейса приложения (ABI) и стандарта вызова процедур (PCS, иногда включаемого в ABI) для вашей целевой платформы (CPU/OS, для некоторых платформ их может быть больше, чем одна версия).
Если PCS фактически позволяет передавать структуру в регистрах, это зависит не только от ее размера, но также от ее положения в списке аргументов и типов предшествующих аргументов. Например, ARM-PCS (AAPCS) упаковывает аргументы в первые 4 регистра до тех пор, пока они не будут заполнены, и передает дальнейшие данные в стек, даже если это означает, что аргумент разбивается (все упрощено, если интересно: документы бесплатны для загрузки из ARM).
Для возвращаемых структур, если они не передаются через регистры, большинство PCS выделяет пространство в стеке вызывающей стороной и передает вызываемому указатель на структуру (неявный вариант). Это идентично локальной переменной в вызывающей стороне и явной передаче указателя для вызываемой стороны. Однако для неявного варианта результат должен быть скопирован в другую структуру, так как нет способа получить ссылку на неявно распределенную структуру.
Некоторые PCS могут делать то же самое для структур аргументов, другие просто используют те же механизмы, что и для скаляров. В любом случае вы откладываете такие оптимизации до тех пор, пока действительно не поймете, что они вам нужны. Также прочтите PCS вашей целевой платформы. Помните, что ваш код может работать еще хуже на другой платформе.
Примечание. Передача структуры через глобальный темп не используется современными PCS, поскольку она не является поточно-ориентированной. Однако для некоторых небольших архитектур микроконтроллеров это может отличаться. В основном, если они имеют только небольшой стек (S08) или ограниченные функции (PIC). Но в большинстве случаев структуры также не передаются в регистрах, поэтому настоятельно рекомендуется передавать по указателю.
Если это только для неизменности оригинала: передайте const mystruct *ptr
, Если вы не выбросили const
это даст предупреждение по крайней мере при записи в структуру. Сам указатель также может быть постоянным: const mystruct * const ptr
,
Итак: нет практического правила; это зависит от слишком многих факторов.
Действительно, лучшее практическое правило, когда дело доходит до передачи структуры в качестве аргумента функции по ссылке или по значению, - это избегать передачи ее по значению. Риски почти всегда перевешивают выгоды.
Для полноты картины я укажу, что при передаче / возврате структуры по значению происходит несколько вещей:
- все элементы структуры копируются в стек
- при возврате структуры по значению снова все члены копируются из стековой памяти функции в новую ячейку памяти.
- операция подвержена ошибкам - если члены структуры являются указателями, распространенной ошибкой является допущение, что вы можете безопасно передавать параметр по значению, поскольку вы работаете с указателями - это может привести к очень трудным обнаружениям ошибок.
- если ваша функция изменяет значение входных параметров и ваши входные данные являются структурными переменными, передаваемыми по значению, вы должны помнить, что ВСЕГДА возвращайте структурную переменную по значению (я видел это довольно много раз). Что означает удвоение времени копирования элементов структуры.
Теперь перейдем к тому, что достаточно мало, с точки зрения размера структуры - так что "стоит" передать ее по значению, это будет зависеть от нескольких вещей:
- соглашение о вызовах: что компилятор автоматически сохраняет в стеке при вызове этой функции (обычно это содержимое нескольких регистров). Если члены вашей структуры могут быть скопированы в стек, используя преимущества этого механизма, то штрафов нет.
- тип данных члена структуры: если регистры вашего компьютера 16-битные, а тип данных членов вашей структуры 64-битный, очевидно, он не поместится в один регистр, поэтому для одной копии придется выполнять несколько операций.
- количество регистров, которые на самом деле имеет ваша машина: при условии, что у вас есть структура только с одним членом, char (8 бит). Это должно вызывать такие же издержки при передаче параметра по значению или по ссылке (теоретически). Но есть потенциально еще одна опасность. Если ваша архитектура имеет отдельные регистры данных и адресов, параметр, передаваемый по значению, займет один регистр данных, а параметр, переданный по ссылке, займет один регистр адреса. Передача параметра по значению оказывает давление на регистры данных, которые обычно используются больше, чем адресные регистры. И это может привести к разливу в стеке.
Итог - очень сложно сказать, когда нормально передавать структуру по значению. Безопаснее просто не делать этого:)
Примечание: причины, по которым так или иначе совпадают.
Когда передавать / возвращать по значению:
- Объект является фундаментальным типом, как
int
,double
указатель - Бинарная копия объекта должна быть сделана - и объект не большой.
- Скорость важна, и передача по значению быстрее.
Объект концептуально малочисленный
struct quaternion { long double i,j,k; } struct pixel { uint16_t r,g,b; } struct money { intmax_t; int exponent; }
Когда использовать указатель на объект
- Не уверен, что значение или указатель на значение лучше - так что это выбор по умолчанию.
- Объект большой.
- Скорость важна, и проход по указателю на объект быстрее.
- Использование стека имеет решающее значение. (Строго говоря, это может способствовать стоимости в некоторых случаях)
- Модификации переданного объекта необходимы.
Объект нуждается в управлении памятью.
struct mystring { char *s; size_t length; size_t size; }
Примечания: Напомним, что в Си ничего не передается по ссылке. Даже передача указателя передается по значению, так как значение указателя копируется и передается.
Я предпочитаю проходные номера, будь они int
или же pixel
по значению, поскольку концептуально легче понять код. Передача чисел по адресу концептуально немного сложнее. С большими числовыми объектами, это может быть быстрее, чтобы передать по адресу.
Объекты, для которых передан их адрес, могут использовать restrict
чтобы сообщить функции объекты не перекрываются.
На типичном ПК производительность не должна быть проблемой даже для довольно больших структур (многие десятки байтов). Следовательно, важны другие критерии, особенно семантика: вы действительно хотите работать с копией? Или для того же объекта, например, при манипулировании связанными списками? Руководящий принцип должен состоять в том, чтобы выразить желаемую семантику с помощью наиболее подходящей языковой конструкции, чтобы сделать код читабельным и поддерживаемым.
Тем не менее, если есть какое-либо влияние на производительность, это может быть не так ясно, как можно было бы подумать.
Memcpy работает быстро, и локальность памяти (что хорошо для стека) может быть более важной, чем размер данных: все копирование может происходить в кеше, если вы передадите и вернете struct by value в стеке. Кроме того, при оптимизации возвращаемого значения следует избегать избыточного копирования локальных переменных, которые должны быть возвращены (что наивные компиляторы делали 20 или 30 лет назад).
Передача указателей приводит к появлению псевдонимов в ячейках памяти, которые затем не могут более эффективно кэшироваться. Современные языки часто более ориентированы на ценность, потому что все данные изолированы от побочных эффектов, что улучшает способность компилятора оптимизировать.
Суть в том, да, если вы не столкнетесь с проблемами, не стесняйтесь переходить по значению, если это более удобно или целесообразно. Это может быть даже быстрее.
Мы не передаем структуры по значению, а также не используем голые указатели (ох!) постоянно и везде. Пример.
ERR_HANDLE mx_multiply ( MX_HANDLE result, MX_HANDLE left, MX_HANDLE right ) ;
- результат слева и справа - это экземпляры одного и того же типа (структуры) для 2D-матрицы.
- умножить - это какой-то другой тип ошибки (структуры)
- «дескриптор» — это адрес структуры на «плите» памяти, предварительно выделенной для экземпляров одного и того же типа.
это безопасно? Очень. Это медленно? Немного медленнее по сравнению с голыми указателями.
Абстрактным образом набор значений данных, передаваемых в функцию, представляет собой структуру по значению, хотя и не объявленную как таковую. Вы можете объявить функцию как структуру, в некоторых случаях требуя определения типа. когда вы делаете это все в стеке. и это проблема. помещая значения ваших данных в стек, становится уязвимым для перезаписи, если функция или подпрограмма вызывается с параметрами перед использованием или копированием данных в другом месте. Лучше всего использовать указатели и классы.