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

Структуру можно передать или вернуть по значению или передать/вернуть по ссылке (через указатель) в 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: Не знаю, почему это стало помеченным как возможный дубликат, поскольку я на самом деле ссылаюсь на другой вопрос в моем вопросе. Я прошу разъяснить, что представляет собой небольшую структуру, и я хорошо знаю, что большинство структур времени следует передавать по ссылке.

Ответ 1

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

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

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

Ответ 2

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

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

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

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

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

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

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

Ответ 3

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

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

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

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

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

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

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

Ответ 4

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

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

  • все элементы структуры копируются в стек
  • возвращая struct по значению, снова все члены копируются из памяти стека функций в новую ячейку памяти.
  • операция с ошибкой - если элементы структуры являются указателями, общая ошибка состоит в том, чтобы предположить, что вы можете передать параметр по значению, поскольку вы работаете с указателями - это может вызвать очень трудное выявление ошибок.
  • если ваша функция изменяет значение входных параметров, а ваши входы представляют собой переменные структуры, переданные по значению, вы должны помнить, чтобы ВСЕГДА возвращали структурную переменную по значению (я видел это несколько раз). Это означает удвоение времени копирования элементов структуры.

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

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

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

Ответ 5

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

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

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

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

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

Ответ 6

Примечание. Причины, чтобы сделать это одним или другим способом перекрытия.

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

  • Объект - это фундаментальный тип, например int, double, указатель.
  • Необходимо создать двоичную копию объекта - и объект невелик.
  • Скорость важна и скорость передачи быстрее.
  • Объект концептуально является маленьким числовым

    struct quaternion {
      long double i,j,k;
    }
    struct pixel {
      uint16_t r,g,b;
    }
    struct money {
      intmax_t;
      int exponent;
    }
    

Когда использовать указатель на объект

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

    struct mystring {
      char *s;
      size_t length;
      size_t size;
    }
    

Примечания: Вспомните, что в C ничто действительно не передается по ссылке. Даже передача указателя передается по значению, поскольку значение указателя копируется и передается.

Я предпочитаю передавать числа, будь они int или pixel по значению, поскольку концептуально легче понять код. Передача чисел по адресу концептуальна немного сложнее. С более крупными числовыми объектами, возможно, быстрее пройти по адресу.

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

Ответ 7

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

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

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

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

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

Ответ 8

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