Безопасно ли выделять слишком мало места (если вы знаете, что оно вам не понадобится)? - PullRequest
14 голосов
/ 09 сентября 2011

Таким образом, C99 благословил обычно используемый хак с «гибким элементом массива», позволив нам сделать struct s, которые могут быть перераспределены в соответствии с нашими требованиями к размеру. Я подозреваю, что это вполне безопасно на большинстве вменяемых реализаций, чтобы сделать это, но законно ли в C "отменить выделение", если мы знаем в определенных ситуациях, что нам не понадобятся некоторые члены struct?

Абстрактный пример

Скажите, что у меня есть тип:

struct a {
  bool   data_is_x;
  void * data;
  size_t pos;
};

Если data_is_x, то тип data - это тип, который должен использовать член pos. В противном случае функциям, которые работают с этим struct, не понадобится член pos для этой конкретной копии struct. По сути, struct несет в себе информацию о том, есть ли у него pos член, и эта информация не будет изменена в течение жизни struct (за исключением злого вреда, который все равно в значительной степени сломает) , Можно ли с уверенностью сказать:

struct a *a = malloc(data_is_x ? sizeof(struct a) : offsetof(struct a, pos));

который выделит место для pos члена, только если он нужен? Или это нарушает ограничение на использование пространства приведения, которое слишком мало для указателя struct, даже если вы никогда не используете рассматриваемые элементы?

Конкретный пример

Мой реальный пример использования немного сложен; это здесь в основном, чтобы вы могли понять почему Я хочу сделать это:

typedef struct {
  size_t size;
  void * data;
  size_t pos;
} mylist;

Код для mylist_create указывает, что для size > 0, data - это массив непрерывных данных, длина которого size элементов (независимо от того, какой элемент может быть), но для size == 0 это текущий узел двусвязного списка, содержащего элементы. Все функции, которые работают с mylist s, проверят, является ли size == 0. Если это так, они будут обрабатывать данные в виде связанного списка с «текущим» индексом, на который указывает узел data. Если нет, они будут обрабатывать данные как массив с «текущим» индексом, хранящимся в pos.

Теперь, если size == 0, нам на самом деле не нужен pos член, но если size > 0, мы будем нуждаться. Поэтому мой вопрос: законно ли это делать:

mylist *list = malloc(size ? sizeof(mylist) : offsetof(mylist, pos));

Если мы гарантируем (на штраф за неопределенное поведение), что, в то время как size == 0, мы никогда не будем пытаться (или должны) получить доступ к члену pos? Или где-то в стандарте сказано, что UB даже думать об этом?

Ответы [ 5 ]

4 голосов
/ 09 сентября 2011

malloc сама по себе не заботится, сколько памяти вы выделяете для структуры, это разыменование памяти вне блока, который не определен.Начиная с C99 6.5.3.2 Address and indirection operators:

Если указателю присвоено недопустимое значение, поведение унарного оператора * не определено.

И с 7.20.3 Memory management functions мы находим (мой курсив):

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

Следовательно, вы можете сделать что-то вроде:

typedef struct { char ch[100]; } ch100;
ch100 *c = malloc (1);

и, если вы когда-либо пытаетесь сделать что-либо только с c->ch[0], это вполне приемлемо.


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

Насколько я понимаю, у вас есть структура:

typedef struct {
  size_t size;
  void * data;
  size_t pos;
} mylist;

, где вы хотите использовать только data, где size равно 0, и оба data и pos, где size больше 0. Это исключает использование символов data и posв объединении.

Значительное число реализаций malloc округляет запрошенное пространство до кратного 16 байт (или некоторой большей степени двух), чтобы облегчить проблемы фрагментации памяти.Конечно, это не требуется стандартом, но это довольно часто.

Предполагая (например) 32-битные указатели и size_t, ваши двенадцать байтов структуры, скорее всего, займут 16-заголовок байтовой арены и 16-байтовый блок данных.Этот блок будет по-прежнему иметь размер 16 байт, даже если вы запросите только 8 (т. Е. Без pos).

Если бы у вас был 64-битный указатель и size_t типов, это могло бы иметь значение - 24 байтас pos и 16 без.

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

2 голосов
/ 09 сентября 2011

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

struct leaf_node {
    size_t size;
    void *data;
    size_t pos;
};
struct linked_node {
    size_t size;
    void *next;
};

void *in = ...;

if (*(size_t*)(in) == 0) {
    struct leaf_node *node = in;
    ...
} else {
    struct linked_node *node = in;
    ....
}

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

Что сказал paxdiablo о минимальном размере 16 байтов на 32-битные системы часто бывают верными, но вы все равно можете выделить большие куски, чтобы обойти это.

В 32-битной системе для объекта connected_node будет использоваться 8 байтов.Вы должны использовать пулы, чтобы извлечь выгоду из того, что вы пытаетесь сделать.

struct leaf_node *leaf_pool = malloc(N*sizeof(struct leaf_node));
struct linked_node *linked_pool = malloc(N*sizeof(struct linked_node));

Конечно, вы никогда не должны перераспределять пулы, но выделять новые пулы по мере необходимости и повторно использовать узлы.В этом случае один leaf_node будет использовать 12 байтов.

То же самое относится к linked_node, который будет использовать 8 байтов вместо 16 байтов, если вы разместите их в пуле.

Тамне будет узким местом в производительности, пока ваши структуры не используют __attribute__ ((packed)) в GCC, и в этом случае ваши структуры могут быть очень плохо выровнены.Особенно, если у вас есть, например, дополнительный символ в вашей структуре, дающий ему размер 13 байт.

Теперь, если мы вернемся к вашему первоначальному вопросу, указатель, который вы используете для указания на распределенные данные, неважно, чтобы вы не обращались к данным вне буфера.Ваша структура по существу похожа на строку символов, и вы проверяете, является ли первый размер size_t «нулевым байтом», если он тогда предполагается, что буфер меньше.Если он не нулевой, то предполагается, что «строка» длиннее, и вы читаете больше данных.Вовлечены точно такие же риски, и единственная разница после компиляции - это размер каждого прочитанного элемента.Нет ничего волшебного в использовании [el] для строк по сравнению с приведением к указателю структуры и чтением элементов, так как вы можете проверить это, просто вызвав segfault с помощью [el].

1 голос
/ 10 сентября 2011

Вы можете подумать, что вы экономите 4 или 8 байтов, но ваше распределение памяти может быть выровнено.Если вы используете gcc и его 16-байтовое выравнивание, вы можете получить что-то похожее на это.используйте malloc (0) или malloc (24), используется тот же объем памяти.

1 голос
/ 09 сентября 2011

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

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

0 голосов
/ 09 сентября 2011

Экономия 4 байтов в распределении практически бессмысленна, если вы не говорите о многих десятках тысяч из них, и в этом случае вы, вероятно, захотите использовать схему распределения пула с "освобожденными" структурами временно, но на список «доступных» («пул») вместо того, чтобы постоянно их освобождать и перераспределять. Я гарантирую, что это будет быстрее. Но для правильного использования такой схемы все повторно используемые части должны быть легко взаимозаменяемыми, то есть иметь элемент size_t pos.

Итак, да, то, что вы думаете делать, совершенно законно; Я просто не уверен, что это стоит осложнений и нехватки гибкости, которую это налагает.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...