Безопасно ли выделять слишком мало места (если вы знаете, что вам это не понадобится)?

Итак, C99 благословил часто используемый "элемент гибкого массива", чтобы позволить нам сделать struct, который можно было бы комбинировать в соответствии с нашими требованиями к размеру. Я подозреваю, что это абсолютно безопасно для большинства разумных реализаций, но это законно в 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 это текущий node двусвязного списка, содержащего элементы. Все функции, которые работают с mylist, будут проверять, будет ли size == 0. Если это так, они будут обрабатывать данные в виде связанного списка с "текущим" индексом, в зависимости от того, что указывает node data. Если нет, они будут обрабатывать данные как массив с "текущим" индексом, хранящимся в pos.

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

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

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

Ответы

Ответ 1

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

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

И, из 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

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

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-битной системе связанный_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, и в этом случае ваши структуры могут сильно выровняться. Особенно, если у вас есть, например, дополнительный char в вашей структуре, дающий размер 13 байт.

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

Ответ 3

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

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

Ответ 4

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

for (int i = 0; i <= 64; i++) {
    char *p = (char *) malloc(i);
    char *q = (char *) malloc(i);
    long long t = q - p;
    cout << "malloc(" << i << ") used " << t << " bytes " << endl;
}

печатает

malloc(0) used 32 bytes 
malloc(1) used 32 bytes 
malloc(2) used 32 bytes 
malloc(3) used 32 bytes 
malloc(4) used 32 bytes 
malloc(5) used 32 bytes 
malloc(6) used 32 bytes 
malloc(7) used 32 bytes 
malloc(8) used 32 bytes 
malloc(9) used 32 bytes 
malloc(10) used 32 bytes 
malloc(11) used 32 bytes 
malloc(12) used 32 bytes 
malloc(13) used 32 bytes 
malloc(14) used 32 bytes 
malloc(15) used 32 bytes 
malloc(16) used 32 bytes 
malloc(17) used 32 bytes 
malloc(18) used 32 bytes 
malloc(19) used 32 bytes 
malloc(20) used 32 bytes 
malloc(21) used 32 bytes 
malloc(22) used 32 bytes 
malloc(23) used 32 bytes 
malloc(24) used 32 bytes 
malloc(25) used 48 bytes 
malloc(26) used 48 bytes 
malloc(27) used 48 bytes 
malloc(28) used 48 bytes 
malloc(29) used 48 bytes 
malloc(30) used 48 bytes 
malloc(31) used 48 bytes 
malloc(32) used 48 bytes 
malloc(33) used 48 bytes 
malloc(34) used 48 bytes 
malloc(35) used 48 bytes 
malloc(36) used 48 bytes 
malloc(37) used 48 bytes 
malloc(38) used 48 bytes 
malloc(39) used 48 bytes 
malloc(40) used 48 bytes 
malloc(41) used 64 bytes 
malloc(42) used 64 bytes 
malloc(43) used 64 bytes 
malloc(44) used 64 bytes 
malloc(45) used 64 bytes 
malloc(46) used 64 bytes 
malloc(47) used 64 bytes 
malloc(48) used 64 bytes 
malloc(49) used 64 bytes 
malloc(50) used 64 bytes 
malloc(51) used 64 bytes 
malloc(52) used 64 bytes 
malloc(53) used 64 bytes 
malloc(54) used 64 bytes 
malloc(55) used 64 bytes 
malloc(56) used 64 bytes 
malloc(57) used 80 bytes 
malloc(58) used 80 bytes 
malloc(59) used 80 bytes 
malloc(60) used 80 bytes 
malloc(61) used 80 bytes 
malloc(62) used 80 bytes 
malloc(63) used 80 bytes 
malloc(64) used 80 bytes 

В зависимости от вашей системы может случиться так, что если вы используете malloc (0) или malloc (24), используется тот же объем памяти.

Ответ 5

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

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