Есть ли недостатки в передаче структур по значению в C, а не в передаче указателя? - PullRequest
149 голосов
/ 02 октября 2008

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

Ответы [ 10 ]

188 голосов
/ 02 октября 2008

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

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

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

61 голосов
/ 03 октября 2008

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

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

См .: http://gcc.gnu.org/onlinedocs/gcc/Code-Gen-Options.html

-fpcc-структура-возвращение

-freg-структура-возвращение

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

19 голосов
/ 28 июля 2010

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

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

15 голосов
/ 02 октября 2008

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

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

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

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

9 голосов
/ 02 октября 2008

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

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

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

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

9 голосов
/ 02 октября 2008

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

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

9 голосов
/ 02 октября 2008

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

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

8 голосов
/ 26 сентября 2011

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

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

5 голосов
/ 21 апреля 2011

Страница 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                 |
        +---------------------------+
0 голосов
/ 05 сентября 2016

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

...