Является ли эта реализация mallo c распределителем ударов? - PullRequest
1 голос
/ 29 мая 2020

Недавно я написал небольшой malloc и подумал, не было ли это выделением смещения. Мне интересно это, потому что (поправьте меня, если я ошибаюсь), я считаю, что фактический malloc (при использовании mmap вместо sbrk) использует тот же метод (вроде), но выделитель ударов просто увеличивает местоположение кучи. Вот мой код:

#include <cstdio>
#include <cstddef>
#include <unistd.h>

#define word_size sizeof(intptr_t)
#define align(n) ((n + word_size - 1) & ~(word_size - 1))

void* my_malloc(size_t size) {
    void* p = sbrk(0);
    if (sbrk(align(size)) == (void*) -1)
        return NULL; // failed
    return p;
}

int main() {
    int* foo = (int*) my_malloc(1);
    *foo = 100;
    printf("%d\n", *foo);
}

1 Ответ

2 голосов
/ 29 мая 2020

Итак, я никогда раньше не слышал термин «выдвижной распределитель», но концепция проста.

Это действительно наивный распределитель, который может быть очень быстрым из-за небольшого количества операций по обслуживанию, но у вас есть жить с довольно тяжелым ограничением: нет "бесплатной" операции для отдельного запроса - вы просто уничтожаете все.

В вашем случае вы звоните sbrk(0), чтобы получить первый адрес в конце всего сегмента данных программы - это будет возвращаемое значение - затем "увеличьте" конец памяти с помощью sbrk(nbytes) после соответствующего округления в большую сторону.

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

static void *bump_arena_start = 0;

void* my_malloc(size_t size) {
    void* p = sbrk(0);

    if (bump_arena_start == 0) bump_arena_start = p;

    if (sbrk(align(size)) == (void*) -1)
        return NULL; // failed
    return p;
}

void destroy_bump_arena(void)
{
    if (bump_arena_start) brk(bump_arena_start);
}

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

Пихта st: предполагается, что никто другой не выделяет память, ему придется переопределить все другие операции: malloc, C++ new, все во время выполнения C, et c.

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

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

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

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

Это просто действительно специализированный вариант использования, который, вероятно, в целом бесполезен, и, если вы не выполняете встроенную работу (где вы, по сути, контролируете свою собственную среду выполнения), вы действительно не могли бы сделать один с системой ломается таким образом.

Примечание: у вас могут возникнуть проблемы с выравниванием, если у вас есть объекты, превышающие размер целочисленного указателя. Что, если бы вы сделали это?

   int      *foo1 = my_malloc(sizeof(int));       // 8 bytes (usually)
   __int128 *foo2 = my_malloc(sizeof(__int128));  // 16 bytes

Наивное выравнивание поместило бы int на 8-байтовую границу, но также и 128-битное значение (которое составляет 16 байтов), не выровненное по собственному размеру ; на некоторых платформах это, вероятно, ошибка, и это почти всегда неэффективно.

Чтобы сделать это правильно, вы должны запросить текущее следующее значение через sbrk(0) и убедиться, что это было правильно выровнено для размер, возможно, немного увеличив разрыв.

РЕДАКТИРОВАТЬ : Я изучил это немного больше, и довольно ясно, что ваш пример не совсем считается распределителем ударов . Вот почему.

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

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

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

Действительно полезная ссылка: https://os.phil-opp.com/allocator-designs/

EDIT2 - подробнее о выравнивании по запросу.

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

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

двухбайтовый short, он потратит 14 байтов после него. Это может не иметь большого значения для вашего приложения, или это может быть что-то.

Я никогда не писал распределитель памяти, поэтому я здесь много размахиваю руками, но любой, кто использует распределитель ударов, имеет некоторые специализированные требования и, вероятно, подходит для выполнения специализированных запросов. 1078 *, что требуемое выравнивание, сохраните это как возвращаемое значение, затем вызовите sbrk(size), чтобы поднять конечный маркер.

Обратите внимание, что вы не выравниваете размер выделения, но только размер элемента нижнего уровня: запрос массива из 20 short значений означает, что вы запрашиваете 40 байтов, но с выравниванием 2, а 100 байтов для строки означает, что вы просто берете следующие 100 байтов w / o любое выравнивание.

void *my_malloc(size_t nbytes, size_t align = 8)
{
    void *p = sbrk(0);
    p += round up but too hard to think on Friday
    sbrk(nbytes);
    num_allocations++;
    return p;
}

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

Опять же: я в основном просто выдумываю, мне никогда не приходилось думать об этом таким образом, но я знаю, что если я работаю над ограниченным объемом памяти Платформа, такая как Arduino с RAM, измеряемой в килобайтах , вы должны подумать об этом.

...