Есть ли недостатки в передаче структур по значению в C, а не в передаче указателя?
Есть ли недостатки в передаче структур по значению в C, а не в передаче указателя?
Если структура велика, очевидно, что существует аспект производительного копирования большого количества данных, но для структуры меньшего размера она должна быть в основном такой же, как и передача нескольких значений в функцию.
Это может быть даже более интересно, когда используется в качестве возвращаемых значений. C имеет только одно возвращаемое значение из функций, но вам часто нужно несколько. Итак, простое решение - поместить их в структуру и вернуть.
Есть ли причины для этого или против?
Поскольку не всем понятно, о чем я здесь говорю, приведу простой пример.
Если вы программируете на C, вы рано или поздно начнете писать функции, которые выглядят так:
void examine_data(const char *ptr, size_t len)
{
...
}
char *p = ...;
size_t l = ...;
examine_data(p, l);
Это не проблема. Единственная проблема заключается в том, что вы должны согласиться со своим коллегой в том порядке, в котором должны быть параметры, чтобы вы использовали одинаковое соглашение во всех функциях.
Но что происходит, когда вы хотите вернуть такую же информацию? Обычно вы получаете что-то вроде этого:
char *get_data(size_t *len);
{
...
*len = ...datalen...;
return ...data...;
}
size_t len;
char *p = get_data(&len);
Это прекрасно работает, но гораздо более проблематично. Возвращаемое значение является возвращаемым значением, за исключением того, что в этой реализации это не так. Из вышесказанного невозможно сказать, что функция get_data не может посмотреть, на что указывает len. И нет ничего, что заставляет компилятор проверять, что значение фактически возвращается через этот указатель. Так что в следующем месяце, когда кто-то другой изменяет код, не понимая его должным образом (потому что он не читал документацию?), Он ломается, никто не замечает, или он начинает аварийно падать.
Итак, решение, которое я предлагаю, это простая структура
struct blob { char *ptr; size_t len; }
Примеры можно переписать так:
void examine_data(const struct blob data)
{
... use data.tr and data.len ...
}
struct blob = { .ptr = ..., .len = ... };
examine_data(blob);
struct blob get_data(void);
{
...
return (struct blob){ .ptr = ...data..., .len = ...len... };
}
struct blob data = get_data();
По некоторым причинам, я думаю, что большинство людей инстинктивно заставляют exam_data брать указатель на структурный объект, но я не понимаю, почему. Он по-прежнему получает указатель и целое число, просто гораздо яснее, что они идут вместе. А в случае с get_data невозможно все испортить, как я описал ранее, так как для длины нет входного значения и должна быть возвращаемая длина.
11 ответов
Для небольших структур (например, точка, прямоугольник) передача по значению вполне приемлема. Но, кроме скорости, есть еще одна причина, по которой вы должны быть осторожны при передаче / возврате больших структур по значению: пространство в стеке.
Большая часть программирования на C предназначена для встраиваемых систем, где объем памяти ограничен, а размеры стеков могут измеряться в килобайтах или даже байтах... Если вы передаете или возвращаете структуры по значению, копии этих структур будут размещены на стек, потенциально вызывая ситуацию, что этот сайт назван в честь...
Если я вижу приложение, которое, кажется, использует слишком много стеков, структуры, переданные по значению, - это одна из вещей, которые я ищу в первую очередь.
Одна из причин не делать этого, которая не была упомянута, состоит в том, что это может вызвать проблему, где бинарная совместимость имеет значение.
В зависимости от используемого компилятора структуры могут передаваться через стек или регистры в зависимости от параметров / реализации компилятора.
Смотрите: http://gcc.gnu.org/onlinedocs/gcc/Code-Gen-Options.html
-fpcc-структура обратного
-freg-структура обратного
Если два компилятора не согласны, все может взорваться. Само собой разумеется, что основными причинами, по которым этого не делается, являются потребление стека и производительность.
Чтобы действительно ответить на этот вопрос, нужно углубиться в землю собрания:
(В следующем примере используется gcc для x86_64. Любой желающий может добавить другие архитектуры, такие как MSVC, ARM и т. Д.)
Давайте иметь наш пример программы:
// foo.c
typedef struct
{
double x, y;
} point;
void give_two_doubles(double * x, double * y)
{
*x = 1.0;
*y = 2.0;
}
point give_point()
{
point a = {1.0, 2.0};
return a;
}
int main()
{
return 0;
}
Скомпилируйте его с полной оптимизацией
gcc -Wall -O3 foo.c -o foo
Посмотрите на сборку:
objdump -d foo | vim -
Вот что мы получаем:
0000000000400480 <give_two_doubles>:
400480: 48 ba 00 00 00 00 00 mov $0x3ff0000000000000,%rdx
400487: 00 f0 3f
40048a: 48 b8 00 00 00 00 00 mov $0x4000000000000000,%rax
400491: 00 00 40
400494: 48 89 17 mov %rdx,(%rdi)
400497: 48 89 06 mov %rax,(%rsi)
40049a: c3 retq
40049b: 0f 1f 44 00 00 nopl 0x0(%rax,%rax,1)
00000000004004a0 <give_point>:
4004a0: 66 0f 28 05 28 01 00 movapd 0x128(%rip),%xmm0
4004a7: 00
4004a8: 66 0f 29 44 24 e8 movapd %xmm0,-0x18(%rsp)
4004ae: f2 0f 10 05 12 01 00 movsd 0x112(%rip),%xmm0
4004b5: 00
4004b6: f2 0f 10 4c 24 f0 movsd -0x10(%rsp),%xmm1
4004bc: c3 retq
4004bd: 0f 1f 00 nopl (%rax)
Исключая nopl
подушечки, give_two_doubles()
имеет 27 байтов в то время как give_point()
имеет 29 байтов. С другой стороны, give_point()
дает на одну инструкцию меньше, чем give_two_doubles()
Что интересно, мы заметили, что компилятор смог оптимизировать mov
в более быстрые варианты SSE2 movapd
а также movsd
, Более того, give_two_doubles()
на самом деле перемещает данные из памяти, что замедляет процесс.
По-видимому, большая часть этого может быть неприменима во встроенных средах (где игровое поле для C в настоящее время большую часть времени). Я не мастер сборки, поэтому любые комментарии приветствуются!
Люди здесь забыли упомянуть (или я упустил это из виду), что структуры обычно имеют отступы!
struct {
short a;
char b;
short c;
char d;
}
Каждый символ равен 1 байту, каждый короткий - 2 байта. Насколько велика структура? Нет, это не 6 байтов. По крайней мере, в более распространенных системах. В большинстве систем это будет 8. Проблема в том, что выравнивание не является постоянным, оно зависит от системы, поэтому одна и та же структура будет иметь разное выравнивание и разные размеры в разных системах.
Мало того, что заполнение еще больше пожирает ваш стек, это также добавляет неопределенность в невозможности заранее предсказать заполнение, если вы не знаете, как работает ваша система, а затем посмотрите на каждую структуру, имеющуюся в вашем приложении, и рассчитаете размер. для этого. Передача указателя занимает предсказуемое количество места - нет никакой неопределенности. Размер указателя известен системе, он всегда равен, независимо от того, как выглядит структура, а размеры указателя всегда выбираются таким образом, чтобы они были выровнены и не нуждались в заполнении.
Простое решение будет возвращать код ошибки в качестве возвращаемого значения и все остальное в качестве параметра в функции,
Этот параметр, конечно, может быть структурой, но не вижу особых преимуществ, передавая его по значению, просто отправил указатель.
Передавать структуру по значению опасно, нужно быть очень осторожным с тем, что вы передаете, помните, что в C нет конструктора копирования, если один из параметров структуры является указателем, значение указателя будет скопировано, что может быть очень запутанным и трудным для понимания. поддерживать.
Просто чтобы завершить ответ (полная благодарность Родди), использование стека является еще одной причиной, по которой структура не передается по значению, поверьте мне, отладка переполнения стека является реальной PITA.
Повторите комментарий:
Передача struct по указателю означает, что какая-то сущность владеет этим объектом и полностью знает, что и когда следует выпустить. Передача структуры по значению создает скрытые ссылки на внутренние данные структуры (указатели на другие структуры и т. Д.), В этом трудно поддерживать (возможно, но почему?) .
Вот то, что никто не упомянул:
void examine_data(const char *c, size_t l)
{
c[0] = 'l'; // compiler error
}
void examine_data(const struct blob blob)
{
blob.ptr[0] = 'l'; // perfectly legal, quite likely to blow up at runtime
}
Члены const struct
являются const
, но если этот член является указателем (например, char *
), это становится char *const
а не const char *
мы действительно хотим. Конечно, можно предположить, что const
это документация намерений, и тот, кто нарушает это, пишет плохой код (которым они являются), но этого недостаточно для некоторых (особенно для тех, кто только что потратил четыре часа на поиск причины сбоя).
Альтернативой может быть сделать struct const_blob { const char *c; size_t l }
и использовать это, но это довольно грязно - он сталкивается с той же проблемой схемы именования, которая у меня есть с typedef
указатели. Таким образом, большинство людей придерживаются только двух параметров (или, более вероятно, для этого случая, используя библиотеку строк).
Я думаю, что ваш вопрос подвел итог довольно хорошо.
Еще одно преимущество передачи структур по значению заключается в явном владении памятью. Нет сомнений, что структура находится в куче и кто несет ответственность за ее освобождение.
Я бы сказал, что передача (не слишком большие) структуры по значению, как в качестве параметров, так и в качестве возвращаемых значений, является совершенно законной техникой. Конечно, нужно позаботиться о том, чтобы структура была либо POD-типом, либо семантика копирования задана правильно.
Обновление: Извините, у меня была кепка C++. Я вспоминаю время, когда в C было не разрешено возвращать структуру из функции, но с тех пор это, вероятно, изменилось. Я бы все еще сказал, что это верно, если все компиляторы, которые вы ожидаете использовать, поддерживают эту практику.
На странице 150 Руководства по сборке ПК по http://www.drpaulcarter.com/pcasm/ содержится четкое объяснение того, как C позволяет функции возвращать структуру:
C также позволяет использовать тип структуры в качестве возвращаемого значения функции. Очевидно, что структура не может быть возвращена в регистр EAX. Различные компиляторы обрабатывают эту ситуацию по-разному. Распространенным решением, используемым компиляторами, является внутреннее переписывание функции как функции, которая принимает указатель структуры в качестве параметра. Указатель используется для помещения возвращаемого значения в структуру, определенную вне вызываемой подпрограммы.
Я использую следующий код C, чтобы проверить вышеупомянутое утверждение:
struct person {
int no;
int age;
};
struct person create() {
struct person jingguo = { .no = 1, .age = 2};
return jingguo;
}
int main(int argc, const char *argv[]) {
struct person result;
result = create();
return 0;
}
Используйте "gcc -S" для генерации сборки для этого фрагмента кода C:
.file "foo.c"
.text
.globl create
.type create, @function
create:
pushl %ebp
movl %esp, %ebp
subl $16, %esp
movl 8(%ebp), %ecx
movl $1, -8(%ebp)
movl $2, -4(%ebp)
movl -8(%ebp), %eax
movl -4(%ebp), %edx
movl %eax, (%ecx)
movl %edx, 4(%ecx)
movl %ecx, %eax
leave
ret $4
.size create, .-create
.globl main
.type main, @function
main:
pushl %ebp
movl %esp, %ebp
subl $20, %esp
leal -8(%ebp), %eax
movl %eax, (%esp)
call create
subl $4, %esp
movl $0, %eax
leave
ret
.size main, .-main
.ident "GCC: (Ubuntu 4.4.3-4ubuntu5) 4.4.3"
.section .note.GNU-stack,"",@progbits
Стек перед вызовом create:
+---------------------------+
ebp | saved ebp |
+---------------------------+
ebp-4 | age part of struct person |
+---------------------------+
ebp-8 | no part of struct person |
+---------------------------+
ebp-12 | |
+---------------------------+
ebp-16 | |
+---------------------------+
ebp-20 | ebp-8 (address) |
+---------------------------+
Стек сразу после вызова create:
+---------------------------+
| ebp-8 (address) |
+---------------------------+
| return address |
+---------------------------+
ebp,esp | saved ebp |
+---------------------------+
Я просто хочу указать на одно преимущество передачи ваших структур по значению в том, что оптимизирующий компилятор может лучше оптимизировать ваш код.
Принимая во внимание все, что люди сказали...
- Возврат структуры не всегда был разрешен в C. Теперь это разрешено.
- Возврат структуры может быть выполнен тремя способами... a. Возврат каждого члена в регистр (вероятно, оптимальный, но вряд ли фактический...) b. Возврат структуры в стек (медленнее, чем регистры, но все же лучше, чем холодный доступ к динамической памяти... ура, кеширование!)c. Возвращение структуры в указателе на кучу (это только вредит вам, когда вы читаете или пишете в нее? Хороший компилятор будет передавать указатели, которые он читал только один раз и пытался получить доступ, переупорядочивал инструкции и обращался к ним намного раньше, чем нужно, поэтому он был готов, когда ты был? сделать жизнь лучше? (дрожь))
- Из-за этого разные настройки компилятора могут вызывать разные проблемы при взаимодействии кода. (Регистры разного размера, разное количество заполнения, включена разная оптимизация)
- const-ness или volatile-ness не проникает через структуру и может привести к какой-то жалкой неэффективности или, возможно, привести к поврежденному коду (например, const struct foo не приводит к тому, что foo->bar является константой.)
Некоторые простые меры, которые я предприму после прочтения этого...
- Сделайте так, чтобы ваши функции принимали параметры, а не структуры. Это позволяет точно контролировать константность, изменчивость и т. д., а также гарантирует, что все передаваемые переменные имеют отношение к использующей их функции. Если все параметры одного типа, используйте другой метод для принудительного упорядочения. (Сделайте определения типов, чтобы ваши вызовы функций были более строго типизированными, что обычно делает ОС.)
- Вместо того, чтобы позволять конечной базовой функции возвращать указатель на структуру, созданную в куче, предоставьте указатель на структуру, в которую будут помещены результаты. эта структура все еще может быть в куче, но возможно, что структура на самом деле находится в стеке, и это повысит производительность во время выполнения. Это также означает, что вам не нужно полагаться на компиляторы, предоставляющие вам возвращаемый тип структуры.
- Передавая параметры как части и четко зная константность, изменчивость или ограниченность, вы лучше доносите свои намерения до компилятора, и это позволяет ему лучше оптимизировать.
Я не уверен, где находится «слишком большой» и «слишком маленький», но я думаю, что ответ находится между 2 и количеством регистраций + 1 член. Если бы я создал структуру, содержащую 1 член типа int, то, очевидно, мы не должны передавать эту структуру. (Это не только неэффективно, но и делает намерение ОЧЕНЬ мутным... Я полагаю, что это где-то используется, но не часто)
Если я создам структуру, содержащую два элемента, она может иметь значение в ясности, а компиляторы могут оптимизировать ее в две переменные, которые перемещаются парами. (risc-v указывает, что структура с двумя элементами возвращает оба элемента в регистрах, предполагая, что они являются целыми или меньшими...)
Если я создам структуру, которая содержит столько целых и двойных чисел, сколько есть в регистрах for в процессоре, это ТЕХНИЧЕСКИ возможная оптимизация. Однако в случае, когда я превышаю количество регистров, вероятно, стоило бы сохранить структуру результата в указателе и передавать только те параметры, которые имеют значение. (Это, и, возможно, сделать структуру меньше, а функцию сделать меньше, потому что в настоящее время у нас есть МНОГО регистров в системах, даже во встроенном мире...)