it-swarm.com.ru

объяснение согласованной реализации malloc

Это не домашнее задание, это чисто мое личное образование.

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

#include <stdlib.h>
#include <stdio.h>

void* aligned_malloc(size_t required_bytes, size_t alignment)
{
    void* p1; // original block
    void** p2; // aligned block
    int offset = alignment - 1 + sizeof(void*);
    if ((p1 = (void*)malloc(required_bytes + offset)) == NULL)
    {
       return NULL;
    }
    p2 = (void**)(((size_t)(p1) + offset) & ~(alignment - 1));
    p2[-1] = p1;
    return p2;
}

void aligned_free(void *p)
{
    free(((void**)p)[-1]);
}

void main (int argc, char *argv[])
{
    char **endptr;
    int *p = aligned_malloc (100, strtol(argv[1], endptr, 10));

    printf ("%s: %p\n", argv[1], p);
    aligned_free (p);
}

Реализация работает, но я, честно говоря, не могу понять, как она работает.

Вот что я не могу понять:

  1. Почему нам нужно смещение?
  2. Что делает anding с ~(alignment - 1) выполнить
  3. p2 - это двойной указатель. Почему мы можем вернуть его из функции, которая должна возвращать только один указатель?
  4. Каков общий подход к решению этой проблемы?

Любая помощь очень ценится.

ПРАВКА

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

11
flashburn
  1. Вам нужно смещение, если вы хотите поддерживать выравнивание сверх того, что делает malloc() вашей системы. Например, если ваша система malloc() выравнивает до 8-байтовых границ, и вы хотите выровнять до 16 байт, вы запрашиваете 15 дополнительных байтов, поэтому вы точно знаете, что можете изменить результат, чтобы выровнять его по запросу. Вы также добавляете sizeof(void*) к размеру, который вы передаете malloc(), чтобы оставить место для бухгалтерии.

  2. ~(alignment - 1) - это то, что гарантирует выравнивание. Например, если выравнивание равно 16, то вычтите 1, чтобы получить 15, то есть 0xF, затем отрицание его дает 0xFF..FF0, которая является маской, необходимой для выравнивания любого возвращаемого указателя из malloc(). Обратите внимание, что этот трюк предполагает, что выравнивание является степенью 2 (что обычно бывает, но на самом деле должна быть проверка).

  3. Это void**. Функция возвращает void*. Это нормально, потому что указатель на void - это «Указатель на любой тип», и в этом случае этот тип - void*. Другими словами, преобразование void* в и из других типов указателей разрешено, и двойной указатель все еще является указателем.

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

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

10
John Zwinck

реализация работает

Возможно, но я не был бы слишком уверен. ИМО, тебе лучше работать с первыми принципами. С места в карьер, 

p1 = (void*)malloc

это красный флаг. malloc возвращает void. В C любой указатель может быть назначен из void *. Кастинг из malloc обычно считается плохой формой, потому что любой эффект, который он имеет, может быть только плохим. 

Почему нам нужно смещение

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

p1 извлекается из malloc. Позже он должен быть предоставлен free для освобождения. aligned_malloc резервирует sizeof(void*) байтов в p1, сохраняет там p1 и возвращает p2 (первый «выровненный» адрес в блоке, на который указывает p1). Позже, когда вызывающая сторона передает p2 в aligned_free, она преобразует p2 в действительности в void *p2[] и извлекает исходный p1, используя -1 в качестве индекса. 

Что достигается с помощью ~ (выравнивание - 1)

Это то, что ставит p2 на границе. Скажите, выравнивание 16; alignment -1 составляет 15, 0xF. ~OxF содержит все биты, кроме последних 4. Для любого указателя P, P & ~0xF будет кратно 16. 

p2 - это двойной указатель.

указатель schmointer . malloc возвращает void*. Это блок памяти; вы обращаетесь к нему как хотите. Вы не будете моргать на 

char **args = calloc(7, sizeof(char*));

выделить массив из 7 char * указателей, не так ли? Код выбирает некоторое «выровненное» местоположение, по крайней мере, из sizeof(void*) байтов из p1 и, для целей free, обрабатывает его как void **

Каков общий подход

Там нет одного ответа. Лучше всего использовать стандартную (или популярную) библиотеку. Если вы строите поверх malloc, выделение достаточного количества ресурсов для сохранения «реального» указателя и возвращение выровненного указателя является довольно стандартным, хотя я бы кодировал его иначе. Системный вызов mmap возвращает выровненный указатель страницы, который будет удовлетворять большинству критериев для «выровненного». В зависимости от необходимости, это может быть лучше или хуже, чем использование malloc

2
James K. Lowden

У меня есть несколько проблем с этим кодом. Я собрал их в список ниже:

  1. p1 = (void*)malloc Вы не приводите возвращаемое значение malloc.
  2. free(((void**)p)[-1]); Вы не разыгрываете бесплатно.
  3. if ((p1 = (void*)malloc(required_bytes + offset)) == NULL) Не помещайте присваивание внутри сравнения оператора if. Я знаю, что многие люди делают это, но, на мой взгляд, это просто дурной тон и затрудняет чтение кода.

То, что они делают здесь - это сохранение исходного указателя внутри выделенного блока. Это означает, что только выровненный указатель возвращается пользователю. Фактический указатель, который возвращает malloc, пользователь никогда не видит. Вы должны сохранить этот указатель, потому что он нужен для освобождения блока из выделенного списка и помещения его в свободный список. Во главе каждого блока памяти, malloc помещает некоторую информацию о домашнем хозяйстве. Такие вещи, как указатели next/prev, размер, статус размещения и т.д. .... Некоторые отладочные версии malloc используют защитные слова, чтобы проверить, не переполнилось ли что-либо в буфере. Выравнивание, которое передается в подпрограмму , ДОЛЖНО быть степенью 2.

Когда я написал свою собственную версию malloc для использования в распределителе пула, минимальный размер блока, который я использовал, составлял 8 байт. Таким образом, включая заголовок для 32-разрядной системы, общая сумма составила 28 байт (20 байт для заголовка). В 64-битной системе это было 40 байтов (32 байта для заголовка). Большинство систем имеют повышенную производительность, когда данные выровнены по некоторому значению адреса (4 или 8 байтов в современных компьютерных системах). Причина этого в том, что машина может захватить все Word за один цикл шины, если он выровнен. Если нет, то для получения всего Word требуется два шинных цикла, а затем он должен его сконструировать. Вот почему компиляторы выравнивают переменные по 4 или 8 байтов. Это означает, что последние 2 или 3 бита адресной шины равны нулю.

Я знаю, что существуют некоторые аппаратные ограничения, которые требуют большего выравнивания, чем стандартные 4 или 8. Система Nvidia CUDA, если я правильно помню, требует, чтобы вещи были выровнены по 256 байтам ... и это требование к оборудованию.

Об этом уже спрашивали раньше. Смотрите: Как выделить выровненную память только с использованием стандартной библиотеки?

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

0
Daniel Rudy