Оригинальный ответ
{
void *mem = malloc(1024+16);
void *ptr = ((char *)mem+16) & ~ 0x0F;
memset_16aligned(ptr, 0, 1024);
free(mem);
}
Фиксированный ответ
{
void *mem = malloc(1024+15);
void *ptr = ((uintptr_t)mem+15) & ~ (uintptr_t)0x0F;
memset_16aligned(ptr, 0, 1024);
free(mem);
}
Объяснение по запросу
Первый шаг - выделить достаточно свободного места на всякий случай. Поскольку память должна быть выровнена на 16 байтов (это означает, что адрес начального байта должен быть кратным 16), добавление 16 дополнительных байтов гарантирует, что у нас будет достаточно места. Где-то в первых 16 байтах есть 16-байтовый выровненный указатель. (Обратите внимание , что malloc()
должен возвращать указатель , который достаточно хорошо выровненный для любых . Целей Однако смысл «любых», прежде всего , для таких вещей , как основные типов - long
, double
, long double
, long long
., И указатели на объекты и указатели на функцию Когда вы При выполнении более специализированных задач, таких как игра с графическими системами, им может потребоваться более строгое выравнивание, чем остальной системе - отсюда и такие вопросы и ответы.)
Следующим шагом является преобразование пустого указателя в указатель на символ; GCC, несмотря на это, вы не должны выполнять арифметику указателей на пустых указателях (и GCC имеет опции предупреждения, чтобы сообщить вам, когда вы злоупотребляете им). Затем добавьте 16 к стартовому указателю. Предположим, malloc()
вы вернули вам неправильно выровненный указатель: 0x800001. Добавление 16 дает 0x800011. Теперь я хочу округлить до 16-байтовой границы - поэтому я хочу сбросить последние 4 бита до 0. 0x0F имеет последние 4 бита, равные единице; следовательно, ~0x0F
все биты установлены в один, кроме последних четырех. И, что с 0x800011 дает 0x800010. Вы можете перебрать другие смещения и увидеть, что та же арифметика работает.
Последний шаг, free()
, легко: вы всегда, и только, возврат к free()
значению, один из malloc()
, calloc()
или realloc()
вернулся к вам - все остальное является катастрофой. Вы правильно предоставили, mem
чтобы держать это значение - спасибо. Бесплатные релизы.
Наконец, если вы знаете о внутренних компонентах malloc
пакета вашей системы , вы можете догадаться, что он вполне может вернуть 16-байтовые данные (или 8-байтовые). Если бы он был выровнен по 16 байтам, вам не пришлось бы копаться со значениями. Однако это хитроумно и непереносимо - другие malloc
пакеты имеют разные минимальные выравнивания, и, следовательно, если что-то делать иначе, это приведет к дампам ядра. В широких пределах это решение является переносимым.
Кто-то еще упомянул posix_memalign()
как другой способ получить выровненную память; это не доступно везде, но часто может быть реализовано с использованием этого в качестве основы. Обратите внимание, что было удобно, чтобы выравнивание было степенью 2; другие расстановки сложнее.
Еще один комментарий - этот код не проверяет, что распределение прошло успешно.
поправка
Программист Windows отметил, что вы не можете выполнять операции с битовой маской для указателей, и, действительно, GCC (протестированные 3.4.6 и 4.3.1) действительно жалуется на это. Итак, исправленная версия основного кода - преобразованная в основную программу, следует. Я также позволил себе добавить только 15 вместо 16, как было указано. Я использую, uintptr_t
так как C99 существует достаточно долго, чтобы быть доступным на большинстве платформ. Если бы это было не для использования PRIXPTR
в printf()
утверждениях, было бы достаточно #include <stdint.h>
вместо использования #include <inttypes.h>
. [Этот код включает исправление, указанное CR , который повторял точку зрения, впервые высказанную Биллом К несколько лет назад, которую мне удалось пропустить до сих пор.]
#include <assert.h>
#include <inttypes.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
static void memset_16aligned(void *space, char byte, size_t nbytes)
{
assert((nbytes & 0x0F) == 0);
assert(((uintptr_t)space & 0x0F) == 0);
memset(space, byte, nbytes); // Not a custom implementation of memset()
}
int main(void)
{
void *mem = malloc(1024+15);
void *ptr = (void *)(((uintptr_t)mem+15) & ~ (uintptr_t)0x0F);
printf("0x%08" PRIXPTR ", 0x%08" PRIXPTR "\n", (uintptr_t)mem, (uintptr_t)ptr);
memset_16aligned(ptr, 0, 1024);
free(mem);
return(0);
}
И вот немного более обобщенная версия, которая будет работать для размеров, которые имеют степень 2:
#include <assert.h>
#include <inttypes.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
static void memset_16aligned(void *space, char byte, size_t nbytes)
{
assert((nbytes & 0x0F) == 0);
assert(((uintptr_t)space & 0x0F) == 0);
memset(space, byte, nbytes); // Not a custom implementation of memset()
}
static void test_mask(size_t align)
{
uintptr_t mask = ~(uintptr_t)(align - 1);
void *mem = malloc(1024+align-1);
void *ptr = (void *)(((uintptr_t)mem+align-1) & mask);
assert((align & (align - 1)) == 0);
printf("0x%08" PRIXPTR ", 0x%08" PRIXPTR "\n", (uintptr_t)mem, (uintptr_t)ptr);
memset_16aligned(ptr, 0, 1024);
free(mem);
}
int main(void)
{
test_mask(16);
test_mask(32);
test_mask(64);
test_mask(128);
return(0);
}
Чтобы преобразовать test_mask()
в функцию распределения общего назначения, единственное возвращаемое значение от распределителя должно было бы закодировать адрес выпуска, как несколько человек указали в своих ответах.
Проблемы с интервьюерами
Ури прокомментировал: «Может быть, у меня сегодня утром проблема с пониманием прочитанного, но если вопрос об интервью конкретно говорит:« Как бы вы распределили 1024 байта памяти », а вы явно выделяете больше, чем это? Не будет ли это автоматическим отказом интервьюера?
Мой ответ не помещается в комментарий из 300 символов ...
Это зависит, я полагаю. Я думаю, что большинство людей (включая меня) восприняли вопрос так: «Как бы вы распределили пространство, в котором можно хранить 1024 байта данных, и где базовый адрес кратен 16 байтам». Если интервьюер действительно имел в виду, как вы можете выделить 1024 байта (только) и выровнять его по 16 байтов, то варианты более ограничены.
- Ясно, что одна возможность состоит в том, чтобы выделить 1024 байта и затем дать этому адресу «обработку выравнивания»; проблема с этим подходом состоит в том, что фактическое доступное пространство не является должным образом определенным (используемое пространство составляет от 1008 до 1024 байтов, но не было механизма, позволяющего указать, какой размер), что делает его менее полезным.
- Другая возможность состоит в том, что вы должны написать полный распределитель памяти и убедиться, что 1024-байтовый блок, который вы возвращаете, соответствующим образом выровнен. Если это так, вы, вероятно, в конечном итоге выполните операцию, аналогичную той, которая была предложена, но вы скрываете ее в распределителе.
Однако, если бы интервьюер ожидал какого-либо из этих ответов, я бы ожидал, что они признают, что это решение отвечает на тесно связанный вопрос, а затем пересмотрят свой вопрос, чтобы направить разговор в правильном направлении. (Кроме того, если интервьюер стал действительно неуклюжим, я бы не хотел работать; если ответ на недостаточно точное требование сгорел без исправления, тогда интервьюер - это не тот, для кого безопасно работать.)
Мир движется дальше
Название вопроса недавно изменилось. Это было Решить выравнивание памяти в вопросе C интервью, которое озадачило меня . Пересмотренный заголовок (« Как распределить память только с помощью стандартной библиотеки?» ) Требует немного пересмотренного ответа - это дополнение содержит его.
C11 (ISO / IEC 9899: 2011) добавлена функция aligned_alloc()
:
7.22.3.1 aligned_alloc
Функция
конспект
#include <stdlib.h>
void *aligned_alloc(size_t alignment, size_t size);
Описание функции выделяет пространство для объекта, выравнивание задается , размер которой определяется , а значение которого является неопределенным. Значение должно быть действительным выравниванием, поддерживаемым реализацией, а значение должно быть целым кратным .
aligned_alloc
alignment
size
alignment
size
alignment
Возвращает
The aligned_alloc
функция возвращает либо пустой указатель или указатель на выделенное пространство.
И POSIX определяет posix_memalign()
:
#include <stdlib.h>
int posix_memalign(void **memptr, size_t alignment, size_t size);
ОПИСАНИЕ
posix_memalign()
Функция должна выделить size
байты , выровненные по границе , указанной alignment
, и возвращает указатель на выделенную память в memptr
. Значение alignment
должно быть степенью, кратной двум sizeof(void *)
.
После успешного завершения значение, на которое указывает значение, memptr
должно быть кратным alignment
.
Если размер запрошенного пространства равен 0, поведение определяется реализацией; возвращаемое значение memptr
должно быть либо нулевым указателем, либо уникальным указателем.
free()
Функция должна освободить память, которая ранее была выделена путем posix_memalign()
.
ВОЗВРАЩАЕМОЕ ЗНАЧЕНИЕ
После успешного завершения posix_memalign()
возвращает ноль; в противном случае должен быть возвращен номер ошибки, чтобы указать на ошибку.
Любой или оба из них можно было бы использовать для ответа на вопрос сейчас, но только функция POSIX была опцией, когда на вопрос был первоначально дан ответ.
За кулисами новая выровненная функция памяти выполняет почти ту же работу, что и описанную в вопросе, за исключением того, что она позволяет более легко форсировать выравнивание и отслеживать внутреннее начало выровненной памяти, чтобы код приходится иметь дело специально - он просто освобождает память, возвращаемую функцией выделения, которая использовалась.