Распределитель памяти C и строгий псевдоним - PullRequest
8 голосов
/ 07 октября 2011

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

Очевидно, что это не может быть правдой.Что мне не хватает?Как реализовать распределитель (или пул памяти), который следует строгому псевдониму?

Спасибо.

Редактировать: Позвольте мне прояснить мой вопрос на глупом простом примере:

// s == 0 frees the pool
void *my_custom_allocator(size_t s) {
    static void *pool = malloc(1000);
    static int in_use = FALSE;
    if( in_use || s > 1000 ) return NULL;
    if( s == 0 ) {
        in_use = FALSE;
        return NULL;
    }
    in_use = TRUE;
    return pool;
}

main() {
    int *i = my_custom_allocator(sizeof(int));
    //use int
    my_custom_allocator(0);
    float *f = my_custom_allocator(sizeof(float)); //not allowed...
}

Ответы [ 4 ]

9 голосов
/ 07 октября 2011

Я не думаю, что ты прав.Даже самые строгие из правил псевдонимов будут учитываться только тогда, когда память фактически выделена для определенной цели.Как только выделенный блок был возвращен обратно в кучу с free, на него не должно быть никаких ссылок, и он может быть снова выдан malloc.

И void*, возвращаемым malloc не подчиняется строгому правилу псевдонимов, поскольку стандарт прямо указывает, что указатель void может быть приведен к любому другому виду указателя (и обратно).Раздел 7.20.3 C99 гласит:

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


С точки зрения вашего обновления (пример), где вы не на самом делевернуть память обратно в кучу, я думаю, что ваша путаница возникает, потому что выделенный объект обрабатывается специально.Если вы ссылаетесь на 6.5/6 из C99, вы видите:

Эффективным типом объекта для доступа к его сохраненному значению является объявленный тип объекта, если таковой имеется (сноска 75: Выделенообъекты не имеют объявленного типа).

Перечитайте эту сноску, это важно.

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

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

Для всех других обращений к объекту, не имеющему объявленного типа, эффектive-тип объекта - это просто тип l-значения, используемого для доступа.

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

Если вы поместите туда float, вы должны получить к нему доступ только как float (или совместимый тип).Если вы введете int, вы должны обрабатывать его только как int (или совместимый тип).

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

Итак, если вы должны хранить там int до освобождения в вашем коде, а затем перераспределить его как указатель float, вы должны не попытаться использоватьплавать, пока вы на самом деле не положите туда.До этого момента тип выделенного еще не float.

1 голос
/ 09 января 2017

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

Сложность заключается в том, что заданы структура и функциянапример:

struct someStruct {unsigned char count; unsigned char dat[7]; }
void useStruct(struct someStruct s); // Pass by value

должна быть возможность вызывать его следующим образом:

someStruct *p = malloc(sizeof *p);
p->count = 1;
p->dat[0] = 42;
useStruct(*p);

без предварительной записи всех полей выделенной структуры.Хотя malloc будет гарантировать, что возвращаемый блок выделения может использоваться любым типом, пользовательские функции управления памятью не смогут включить такое повторное использование хранилища, не очистив его байтовым способом (используя цикл или memset).) или использовать free () и malloc () для перезапуска хранилища.

1 голос
/ 28 ноября 2015

Я публикую этот ответ, чтобы проверить мое понимание строгого алиасинга:

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

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

  • Так что если вы пишете через int* и читаете через int*, это нормально.
  • Если вы пишете с использованием int* и читаете float*, это плохо.
  • Если вы пишете с использованием int*, а затем снова пишете с использованием float*, затем считываете его с помощью float*, тогда все в порядке.

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

0 голосов
/ 07 октября 2011

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

Надеюсь, это поможет!

...