Когда я должен передать или вернуть структуру по значению?

структура может быть передана / возвращена значением или передана / возвращена ссылкой (через указатель) в 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 - крупное структуру:

https://github.com/torvalds/linux/blob/b953c0d234bc72e8489d3bf51a276c5c4ec85345/include/linux/sched.h#L1292-1727

что мы хотели бы избежать установка стека любой ценой (особенно с этими стеками режима ядра 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;
};

что лучше правило для определения того, достаточно ли мала структура, чтобы безопасно передавать ее по значению (за исключением смягчающих обстоятельств, таких как глубокая рекурсия)?

наконец, пожалуйста, не говорите мне, что мне нужно профиль. Я просить эвристику использовать, когда я слишком ленив/это не стоит того, чтобы исследовать дальше.

EDIT: у меня есть два следующих вопроса, основанных на ответах до сих пор:

  1. что, если структура на самом деле меньше чем указатель на него?

  2. что делать, если мелкая копия является желаемым поведением (вызываемая функция будет выполнять мелкую копию в любом случае)?

EDIT: не уверен, почему это было отмечено как возможный дубликат, поскольку я фактически связываю другой вопрос в своем вопросе. Я прошу разъяснить, что представляет собой маленький struct и я хорошо знаю, что большую часть времени структуры должны передаваться по ссылке.

8 ответов


на небольших встроенных архитектурах (8/16-биттеры) -- всегда pass by pointer, поскольку нетривиальные структуры не вписываются в такие крошечные регистры, и эти машины, как правило, также страдают от регистра.

на PC-подобных архитектурах (32 и 64 битных процессорах) -- передача структуры по значению в порядкеsizeof(mystruct_t) <= 2*sizeof(mystruct_t*) и функция не имеет много (обычно более 3 машинных слов) других аргументов. В этих условиях типичная оптимизация компилятор будет передавать / возвращать структуру в регистре или паре регистров. Однако на x86-32 этот совет следует принимать с изрядной долей соли, из-за чрезвычайного давления регистра компилятор x86-32 должен иметь дело с -- передача указателя все еще может быть быстрее из-за уменьшения разлива и заполнения регистра.

возврат структуры по значению на PC-нравится, с другой стороны, следует тому же правилу, за исключением того, что когда структура возвращается указатель на структуру заполнено должно быть принят в по указателю , а также-в противном случае вызываемый и вызывающий застряли, чтобы договориться о том, как управлять памятью для этой структуры.


мой опыт, почти 40 лет встроенного в реальном времени, последние 20 с использованием C; это лучший способ передать указатель.

в любом случае необходимо загрузить адрес структуры, а затем вычислить смещение для интересующего поля...

при передаче всей структуры, если она не передается по ссылке, тогда

  1. он не помещается в стек
  2. он копируется, как правило, скрытым вызовом memcpy()
  3. он копируется в раздел памяти, который теперь "зарезервировано" и недоступным для любой другой части программы.

аналогичные соображения существуют, когда структура возвращается по значению.

однако," малые " структуры, то можно вполне держать в работая регистре до 2 передаются в эти регистры особенно если используются определенные уровни оптимизации в составить заявление.

подробности того, что является "малый" зависит от компилятора и базовая аппаратная архитектура.


как структура передается в или из функции, зависит от двоичного интерфейса приложения (ABI) и стандарта вызова процедур (ПК, иногда включенных в ABI) для вашей целевой платформы (CPU/OS, для некоторых платформ может быть более одной версии).

если ПК фактически позволяет передавать структуру в регистрах, это зависит не только от ее размера, но и от ее позиции в списке аргументов и типов предшествующих аргументов. ARM-PCS (AAPCS) для экземпляр упаковывает аргументы в первые 4 регистра, пока они не заполнены, и передает Дальнейшие данные в стек, даже если это означает, что аргумент разделен (все упрощено, если интересно: документы бесплатны для загрузки из ARM).

для возвращаемых структур, если они не передаются через регистры, большинство ПК выделяют пространство в стеке вызывающим объектом и передают указатель на структуру вызываемому объекту (неявный вариант). Это идентично локальной переменной в вызывающем объекте и передаче указатель явно - для вызываемого. Однако для неявного варианта результат должен быть скопирован в другую структуру, так как нет способа получить ссылку на неявно выделенную структуру.

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

Примечание: передача структуры через глобальный temp не используется современными ПК, поскольку она не является потокобезопасной. Однако для некоторых небольших архитектур микроконтроллеров это может быть по-другому. В основном, если у них есть только небольшой стек (S08) или ограниченные функции (PIC). Но в большинстве случаев структуры также не передаются в регистрах,и настоятельно рекомендуется использовать указатель pass-by.

Если это просто для неизменности оригинала: передайте const mystruct *ptr. Если вы откинь const это даст предупреждение, по крайней мере, при записи в структуру. Сам указатель также может быть постоянным: const mystruct * const ptr.

Так: не правило, это зависит от слишком многих факторов.


действительно, лучшее эмпирическое правило, когда дело доходит до передачи структуры в качестве аргумента функции по ссылке vs по значению, - это избегать передачи ее по значению. Риск почти всегда перевешивает выгоду.

для полноты я укажу, что при передаче / возврате структуры по значению происходит несколько вещей:

  1. все члены структуры копируются в стек
  2. если возврат структуры по значению, опять же, все члены копируются из память стека функции в новое место памяти.
  3. операция подвержена ошибкам - если члены структуры являются указателями, распространенной ошибкой является предположение, что вы можете безопасно передать параметр по значению, так как вы работаете с указателями - это может вызвать очень трудно обнаружить ошибки.
  4. если ваша функция изменяет значение входных параметров, а ваши входы являются структурными переменными, передаваемыми по значению, вы должны помнить, что всегда возвращаете структурную переменную по значению (Я видел это несколько раз). Что означает удвоение времени копирования элементов структуры.

теперь перейдем к тому, что достаточно мало означает с точки зрения размера структуры - так что "стоит" передать его по значению, что будет зависеть от нескольких вещей:

  1. соглашение о вызове: что компилятор автоматически сохраняет в стеке при вызове этой функции (обычно это содержимое нескольких регистров). Если элементы структуры могут быть скопированы на стек, использующий этот механизм, чем нет штрафа.
  2. тип данных члена структуры: если регистры вашего компьютера 16 бит, а тип данных членов вашей структуры 64 бит, он, очевидно, не будет вписываться в один регистр, поэтому несколько операций должны быть выполнены только для одной копии.
  3. количество регистров, которое фактически имеет ваша машина: предполагая, что у вас есть структура только с одним членом, char (8bit). Это должно вызвать те же накладные расходы при передаче параметра по значению или по ссылке (в теории). Но потенциально существует еще одна опасность. Если ваша архитектура имеет отдельные регистры данных и адресов, параметр, передаваемый по значению, будет занимать один регистр данных, а параметр, передаваемый по ссылке, будет занимать один регистр адресов. Передача параметра по значению оказывает давление на регистры данных, которые обычно используются больше, чем регистры адресов. И это может причинить расслоины на стек.

итог-очень сложно сказать, когда можно передать структуру по значению. Безопаснее просто не делать этого:)


поскольку часть аргумента, передающая часть вопроса, уже ответила, я сосредоточусь на возвращающейся части.

лучше всего сделать IMO, чтобы не возвращать структуры или указатели на структуры вообще, но передать указатель на "результирующую структуру" функции.

void sum(struct Point* result, struct Point* a, struct Point* b);

это имеет следующие преимущества:

  • на result struct может жить либо в стеке, либо в куче, по усмотрению вызывающего.
  • нет собственности проблемы, так как понятно, что вызывающий отвечает за выделение и освобождение структуры результата.
  • структура может быть даже длиннее, чем необходимо, или быть встроена в более крупную структуру.

Примечание: причины этого так или иначе пересекаются.

когда передать / вернуть по значению:

  1. объект является фундаментальным типом, как int, быстрее передать по адресу.

    объекты, имеющие свой адрес, могут использовать restrict для информирования функции объекты не перекрываются.


на типичном ПК производительность не должна быть проблемой даже для довольно больших структур (много десятков байтов). Следовательно, важны и другие критерии, особенно семантика:вы действительно хотите работать над копией? Или на том же объекте, например, при манипулировании связанными списками? Руководящим принципом должно быть выражение желаемой семантики с наиболее подходящей языковой конструкцией, чтобы сделать код читаемым и доступным для обслуживания.

Что сказал, Если есть любой производительности влияние это может быть не так ясен, как можно было бы подумать.

  • Memcpy быстрый, и локальность памяти (что хорошо для стека) может быть важнее размера данных: копирование может произойти в кэше, если вы передадите и вернете структуру по значению в стеке. Кроме того, оптимизация возвращаемого значения должна избегать избыточного копирования возвращаемых локальных переменных (что наивные компиляторы сделали 20 или 30 лет назад).

  • передача указателей вокруг вводит псевдонимы к местоположениям памяти, которые затем не могут быть кэшированы так же эффективно. Современные языки часто более ориентированы на ценность, поскольку все данные изолированы от побочных эффектов, что улучшает способность компилятора оптимизировать.

нижняя строка-да, если вы не сталкиваетесь с проблемами, не стесняйтесь проходить по значению, если это более удобно или уместно. Может быть, даже быстрее.


абстрактно набор значений данных, передаваемых функции структура по значению, хотя и необъявленной, как таковой. вы можете объявить функцию как структуру, в некоторых случаях требующую определения типа. когда вы делаете это все на стеке. и это проблема. помещая значения данных в стек, он становится уязвимым для записи, если функция или sub вызывается с параметрами перед использованием или копированием данных в другом месте. лучше всего использовать указатели и классы.