Подтвердить что ты не робот

Какие меры предосторожности следует принять, чтобы создать пул памяти, который не вызывает поведение undefined?

Моя первоначальная проблема заключается в том, что у меня есть в проекте несколько объектов, которые имеют пожизненное (то есть, когда я освобожу их, я освобожу их всех), тогда я хотел выделить один блок памяти. У меня есть массивы трех разных типов объектов, struct foo, void * и char. Сначала я хотел malloc() создать такой блок:

// +---------------+---------+-----------+---------+---------+
// | struct foo[n] | padding | void *[m] | padding | char[o] |
// +---------------+---------+-----------+---------+---------+

Но тогда... как я мог выполнить это без вызова поведения undefined? I.e., соблюдая правила псевдонимов типов, aligment... Как правильно рассчитать размер блока памяти, объявить блок памяти (с его эффективным типом) и как правильно получить указатели на все три раздела внутри него портативно?

(я понимаю, что мог бы malloc() 3 блока, что приведет к трем free(), но я хотел бы знать, как это сделать с одним блоком, хотя все еще хорошо себя вести.)

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

4b9b3361

Ответ 1

Как бы вы ни старались, не удалось реализовать malloc в чистом C.

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

Если вы счастливы привязать себя к определенной платформе, тогда вы можете обратиться к этой конкретной реализации malloc для вдохновения.

Но почему бы не рассмотреть возможность записи функции-заглушки, которая вызывает malloc, а также создает таблицу других выделенных объектов? Вы даже можете реализовать какую-то структуру наблюдателей/уведомлений. Другой отправной точкой могут быть известные сборщики мусора, которые были написаны на C.

Ответ 2

Как сказано в другом ответе, вы не можете переопределить malloc внутри самого C. Причина в том, что вы не можете генерировать объекты, у которых нет эффективного типа без malloc.

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

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

Начиная с C11, выравнивание типов может быть получено с помощью оператора _Alignof, а переопределенную память можно запросить с помощью aligned_alloc.

Объедините все это вместе:

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

Анимация тогда не является проблемой, если вы начинаете с объекта, который вы получаете с помощью указателя void*. Каждая часть этого большого объекта имеет эффективный тип, с которым вы вписали, см. Мою недавнюю запись .

Соответствующая часть стандарта C - 6.5 p6:

Эффективный тип объекта для доступа к его сохраненному значению объявленный тип объекта, если таковой имеется .87) Если значение хранится в объект, не имеющий объявленного типа, через значение l, имеющее тип, который не является типом символа, тогда тип lvalue становится эффективный тип объекта для этого доступа и для последующего доступа, которые не изменяют сохраненное значение. Если значение копируется в объект, не имеющий объявленного типа с использованием memcpy или memmove, или скопирован как массив типа символа, тогда эффективный тип измененный объект для этого доступа и для последующих обращений, которые делают не изменять значение - это эффективный тип объекта, из которого значение копируется, если оно есть. Для всех других видов доступа к объект, не имеющий объявленного типа, эффективный тип объекта просто тип lvalue, используемый для доступа.

Здесь "объект без объявленного типа" - это объект (или подобъект), выделенный malloc или аналогичным. В нем четко сказано, что такие объекты могут быть записаны с любым типом в любое время и что это приведет к изменению эффективного типа на желаемый.

Ответ 3

Зная размер union трех типов, может возникнуть более эффективное распределение.

union common {
  struct foo f;
  void * ptr;
  char ch;
};

void *allocate3(struct foo **f, size_t m, void **ptr, size_t n, char **ch,
    size_t o) {
  size_t u_sz = sizeof (union common);
  size_t f_sz = sizeof *f * m;
  size_t f_cnt = (f_sz + u_sz - 1)/u_sz;
  size_t p_sz = sizeof *ptr * n;
  size_t p_cnt = (p_sz + u_sz - 1)/u_sz;
  size_t c_sz = sizeof *ch * o;
  size_t c_cnt = (c_sz + u_sz - 1)/u_sz;
  size_t sum = f_cnt + p_cnt + c_cnt;
  union common *u = malloc(sum * u_sz);
  if (u) {
    *f = &u[0].f;
    *ptr = &u[f_cnt].ptr;
    *ch = &u[f_cnt + c_cnt].ch;
  }
  return u;
}

Таким образом, каждый из 3-х массивов начинается с границы union, поэтому проблемы выравнивания выполняются. Изменяя пространство каждого массива как кратное размеру union, меньше потраченного впустую пространства, чем первый ответ, но соответствует поставленным целям OP.

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

malloc((f_cnt + p_cnt) * u_sz + c_cz);

Дальше подумал о сдавливании выделения. Каждый последующий элемент "count-of-union" может использовать другой союз, который опускает более ранние типы и т.д. Достигнув конца - вот суть идеи чуть выше, последний массив должен только зависеть от последнего типа. Это делает код более сложным (подверженным ошибкам), но при этом увеличивает эффективность использования пространства без проблем с алиментами и т.д. Некоторые идеи кодирования следуют

union common_last2 {
  // struct foo f;
  void * ptr;
  char ch;
};

size_t u2_sz = sizeof (union common_last2);
size_t p_cnt = (p_sz + u2_sz - 1)/u2_sz;

... malloc(f_cnt*usz + p_cnt*u2_sz + c_cz);

*ch = tbd;

Ответ 4

Во-первых, обязательно используйте -fno-strict-aliasing или все, что эквивалентно вашему компилятору. В противном случае, даже если все выравнивания выполнены, компилятор может использовать правила псевдонимов для перекрытия различных применений одного и того же блока памяти.

Я сомневаюсь, что это согласуется с намерением авторов стандарта, но при условии, что оптимизаторы могут быть настолько агрессивными, что безопасный способ использования массивов типа-агностической памяти - это отключить анализ на основе алиасов на основе типов. Авторы Стандарта хотели избежать маркировки как несоответствующих некоторым компиляторам, которые использовали псевдонимы на основе типов. Кроме того, они полагали, что они могут отнестись к мнению писателей-компиляторов о том, как распознавать и обрабатывать случаи, когда вероятность сглаживания вероятна. Они идентифицировали случаи, когда составители компилятора могли не думать, что необходимо распознавать псевдонимы (например, между подписанными и неподписанными типами), но в противном случае ожидали, что авторы компилятора будут судить разумно. Я не вижу никаких доказательств того, что они рассматривали их список допустимых дел как исчерпывающий даже на платформах, где были бы полезны другие формы сглаживания.

Кроме того, независимо от того, насколько тщательно соблюдается Стандарт, нет никакой гарантии, что компиляторы все равно применяют "оптимизацию". По крайней мере, из gcc 6.2 есть ошибки сглаживания, которые будут разбивать код, который использует хранилище как тип X, записывает его как Y, читает его как Y, записывает то же значение, что и X, и считывает хранилище как X-поведение, которое равно 100 %, определенные в стандарте.

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

union
{
   char [POOL_BLOCK_SIZE] dat;
   TYPE_WITH_WORST_ALIGNMENT align;
} memory_pool[POOL_BLOCK_COUNT];

К сожалению, стандарт не дает возможности избежать проблем с псевдонимом на основе типов, даже если все проблемы, связанные с платформой, будут рассмотрены.

Ответ 5

Чтобы ответить на один из вопросов OP

как я мог это сделать (хотел, чтобы malloc() такой блок), не вызывая поведение undefined?

Косвенный неэффективный подход. Выделите union типов. Разумно, если размер меньшего размера не слишком велик.

union common {
  struct foo f;
  void * ptr;
  char ch;
};

void *allocate3(struct foo **f, size_t m, void **ptr, size_t n, char **ch,
    size_t o) {
  size_t sum = m + n + o;
  union common *u = malloc(sizeof *u * sum);
  if (u) {
    *f = &u[0].f;
    *ptr = &u[m].ptr;
    *ch = &u[m + n].ch;
  }
  return u;
}

void sample() {
  struct foo *f;
  void *ptr;
  char *ch;
  size_t m, n, o;
  void *base = allocate3(&f, m, &ptr, n, &ch, o);
  if (base) {
    // use data
  }
  free(base);
}