Есть ли недостатки в передаче структур по значению в C, а не в передаче указателя?

157

Есть ли недостатки в передаче структур по значению в 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 невозможно все испортить, как я описал ранее, так как для длины нет входного значения и должна быть возвращаемая длина.

dkagedal
источник
Для чего это стоит, void examine data(const struct blob)это неверно.
Крис Латс
Спасибо, изменил его, чтобы включить имя переменной.
dkagedal
1
«Из вышесказанного невозможно сказать, что функция get_data не может посмотреть, на что указывает len. И нет ничего, что заставляло бы компилятор проверять, действительно ли значение возвращается через этот указатель». - это не имеет никакого смысла для меня (возможно, потому что ваш пример неверный код из-за последних двух строк, появляющихся вне функции); пожалуйста, вы можете уточнить?
Адам Шпирс
2
Две строки под функцией служат для иллюстрации того, как вызывается функция. Сигнатура функции не дает никаких намеков на то, что реализация должна будет писать только в указатель. И у компилятора нет возможности узнать, что он должен проверить, что значение записано в указатель, поэтому механизм возвращаемого значения может быть описан только в документации.
dkagedal
1
Основная причина, по которой люди не делают этого чаще всего, в Си - историческая. До C89 вы не могли передавать или возвращать структуры по значению, поэтому все системные интерфейсы, предшествующие C89 и логически должны делать это (например gettimeofday), вместо этого используют указатели, и люди берут это в качестве примера.
Звол

Ответы:

202

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

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

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

Родди
источник
2
«Если вы передаете или возвращаете структуры по значению, копии этих структур будут помещаться в стек», я бы назвал braindead любым набором инструментов, который это делает. Да, это печально, что так много людей сделают это, но это не то, к чему призывает стандарт Си. Разумный компилятор оптимизирует все это.
Восстановить Монику
1
@KubaOber Вот почему так часто не делают: stackoverflow.com/questions/552134/…
Родди
1
Есть ли определенная линия, которая отделяет маленькую структуру от большой?
Джози Томпсон
63

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

В зависимости от используемого компилятора структуры могут передаваться через стек или регистры в зависимости от параметров / реализации компилятора.

Смотрите: http://gcc.gnu.org/onlinedocs/gcc/Code-Gen-Options.html

-fpcc-структура обратного

-freg-структура обратного

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

tonylo
источник
4
Это был тот ответ, который я искал.
dkagedal
2
Да, но эти параметры не относятся к передаче по значению. они связаны с возвращением структур, что совсем другое. Возвращение вещей по ссылке - это, как правило, верный способ выстрелить себе в обе ноги. int &bar() { int f; int &j(f); return j;};
Родди
19

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

(В следующем примере используется 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 в настоящее время большую часть времени). Я не мастер сборки, поэтому любые комментарии приветствуются!

оборота kizzx2
источник
6
Подсчет количества инструкций не так уж интересен, если только вы не можете показать огромную разницу или подсчитать более интересные аспекты, такие как количество трудно прогнозируемых переходов и т. Д. Фактические свойства производительности гораздо более тонкие, чем счетчик команд ,
dkagedal
6
@dkagedal: правда. Оглядываясь назад, я думаю, что мой собственный ответ был написан очень плохо. Хотя я не особо сосредоточился на количестве инструкций (не знаю, что произвело на вас такое впечатление: P), на самом деле нужно подчеркнуть, что передача структуры по значению предпочтительнее, чем передача по ссылке для небольших типов. В любом случае, передача по значению предпочтительнее, потому что это проще (без всякого жонглирования, не нужно беспокоиться о том, что кто-то изменяет ваши данные или constвсе время), и я обнаружил, что при копировании с передачей по значению не наблюдается большого снижения производительности (если не выигрыша) вопреки тому, что многие могут поверить.
kizzx2
15

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

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

Повторите комментарий:

Передача struct по указателю означает, что какая-то сущность владеет этим объектом и полностью знает, что и когда следует выпустить. Передача структуры по значению создает скрытые ссылки на внутренние данные структуры (указатели на другие структуры и т. Д.), При этом сложно поддерживать (возможно, но почему?).

Илья
источник
6
Но передача указателя не более «опасна» только потому, что вы помещаете его в структуру, поэтому я не покупаю его.
dkagedal
Отличный момент при копировании структуры, содержащей указатель. Этот момент может быть не очень очевидным. Для тех, кто не знает, что он имеет в виду, сделайте поиск по глубокой копии против мелкой копии.
zooropa
1
Одно из соглашений функции C состоит в том, чтобы выходные параметры были перечислены первыми перед входными параметрами, например, int func (char * out, char * in);
zooropa
Вы имеете в виду, например, как getaddrinfo () помещает выходной параметр последним? :-) Существует тысяча соглашений, и вы можете выбирать, что захотите.
dkagedal
10

Люди, о которых здесь забыли упомянуть (или я упустил из виду), это то, что структуры обычно имеют отступы!

struct {
  short a;
  char b;
  short c;
  char d;
}

Каждый символ равен 1 байту, каждый короткий - 2 байта. Насколько велика структура? Нет, это не 6 байтов. По крайней мере, в более распространенных системах. На большинстве систем это будет 8. Проблема в том, что выравнивание не является постоянным, оно зависит от системы, поэтому одна и та же структура будет иметь разное выравнивание и разные размеры в разных системах.

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

Mecki
источник
2
Да, но дополнение существует без зависимости от передачи структуры по значению или по ссылке.
Илья
2
@dkagedal: Какую часть "разных размеров в разных системах" вы не поняли? Просто потому, что так происходит в вашей системе, вы предполагаете, что оно должно быть таким же, как и в любой другой - именно поэтому вам не следует переходить по значению. Изменен пример, поэтому он не работает в вашей системе.
Меки
2
Я думаю, что комментарии Меки о заполнении структуры особенно актуальны для встраиваемых систем, где размер стека может быть проблемой.
zooropa
1
Я полагаю, что обратная сторона аргумента в том, что если ваша структура - простая структура (содержащая пару примитивных типов), передача по значению позволит компилятору манипулировать ею с помощью регистров - тогда как, если вы используете указатели, все заканчивается память, которая медленнее. Это становится довольно низкоуровневым и в значительной степени зависит от вашей целевой архитектуры, если какой-либо из этих лакомых кусочков имеет значение.
kizzx2
1
Если ваша структура не мала, или у вашего процессора много регистров (а у процессоров Intel нет), данные попадают в стек, и это также память и такая же быстрая / медленная, как и любая другая память. Указатель, с другой стороны, всегда маленький и является просто указателем, а сам указатель обычно всегда заканчивается в регистре при более частом использовании.
Меки
9

Я думаю, что ваш вопрос подвел итог довольно хорошо.

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

Darron
источник
9

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

Обновление: Извините, у меня была кепка C ++. Я вспоминаю время, когда было недопустимо возвращать структуру из функции в C, но с тех пор это, вероятно, изменилось. Я бы все еще сказал, что это верно, если все компиляторы, которые вы собираетесь использовать, поддерживают эту практику.

Грег Хьюгилл
источник
Обратите внимание, что мой вопрос был о C, а не C ++.
dkagedal
Действительно, возвращать структуру из функции просто бесполезно :)
Илья
1
Мне нравится предложение Ильи использовать return как код ошибки и параметры для возврата данных из функции.
zooropa
8

Вот то, что никто не упомянул:

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
}

Члены a const structесть const, но если этот член является указателем (например char *), он становится char *constскорее, чем const char *мы действительно хотим. Конечно, можно предположить, чтоconst это документация намерений, и что любой, кто нарушает это, пишет плохой код (которым они являются), но этого недостаточно для некоторых (особенно для тех, кто только что провел четыре часа, выслеживая причину аварии).

Альтернативой может быть создание struct const_blob { const char *c; size_t l }и использование этого, но это довольно грязно - оно сталкивается с той же проблемой со схемой именования, что и у меня с typedefуказателями. Таким образом, большинство людей придерживаются только двух параметров (или, более вероятно, для этого случая, используя библиотеку строк).

Крис Лутц
источник
Да, это совершенно законно, а также то, что вы хотите иногда делать. Но я согласен с тем, что структурное решение ограничено тем, что вы не можете сделать указатели, на которые они указывают, на const.
dkagedal
Гадкий прием с struct const_blobрешением состоит в том, что даже если const_blobэлементы имеют отличия blobтолько от "косвенной константности", типы struct blob*для a struct const_blob*будут считаться различными в целях строгого правила псевдонимов. Следовательно, если код преобразует a blob*в a const_blob*, любая последующая запись в нижележащую структуру с использованием одного типа автоматически отключит любые существующие указатели другого типа, так что любое использование вызовет неопределенное поведение (которое обычно может быть безвредным, но может быть смертельным) ,
суперкат
5

На странице 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                 |
        +---------------------------+
Цзинго Яо
источник
2
Здесь есть две проблемы. Наиболее очевидным является то, что это вовсе не описывает «как C позволяет функции возвращать структуру». Это только описывает, как это можно сделать на 32-битном x86-оборудовании, которое является одной из наиболее ограниченных архитектур, когда вы смотрите на количество регистров и т. Д. Вторая проблема заключается в том, что компиляторы C генерируют код для возврата значений. определяется ABI (за исключением неэкспортированных или встроенных функций). И, между прочим, встроенные функции, вероятно, являются одним из мест, где возвратные структуры наиболее полезны.
dkagedal
Спасибо за исправления. Подробное описание соглашения о вызовах можно найти на en.wikipedia.org/wiki/Calling_convention .
Цзинго Яо
@dkagedal: Важно не только то, что x86 так поступает, но и существует «универсальный» подход (т.е. этот), который позволит компиляторам для любой платформы поддерживать возврат любого типа структуры, который не такой огромный, чтобы взорвать стек. Хотя компиляторы для многих платформ будут использовать другие более эффективные средства для обработки некоторых возвращаемых значений структурного типа, нет необходимости в том, чтобы язык ограничивал типы возвращаемых структур теми, которые платформа может оптимально обрабатывать.
суперкат
0

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

Vad
источник