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


422

Я только что закончил тест в рамках собеседования, и один вопрос поставил меня в тупик, даже используя Google для справки. Я хотел бы посмотреть, что команда StackOverflow может сделать с этим:

memset_16alignedФункция требует 16 байт , выровненный указатель , переданный ему, или это будет крах.

а) Как бы вы разместили 1024 байта памяти и выровняли ее по 16-байтовой границе?
б) Освободить память после того, memset_16alignedкак выполнено.

{    
   void *mem;
   void *ptr;

   // answer a) here

   memset_16aligned(ptr, 0, 1024);

   // answer b) here    
}

89
хммм ... для долгосрочной жизнеспособности кода, как насчет "Запустите того, кто написал memset_16aligned, исправьте или замените его, чтобы у него не было особого граничного условия"
Стивен А. Лоу,

29
Безусловно актуальный вопрос, который нужно задать - «почему своеобразное выравнивание памяти». Но для этого могут быть веские причины - в этом случае может быть, что memset_16aligned () может использовать 128-битные целые числа, и это проще, если известно, что память выровнена. И т.д.
Джонатан Леффлер

5
Кто бы ни написал memset, он мог бы использовать внутреннее 16-байтовое выравнивание для очистки внутреннего цикла и небольшой пролог / эпилог данных для очистки невыровненных концов. Это было бы намного проще, чем заставлять кодировщиков обрабатывать дополнительные указатели памяти.
Adisak

8
Почему кто-то хочет, чтобы данные были выровнены по 16-байтовой границе? Возможно загрузить его в 128-битные регистры SSE. Я считаю, что (более новые) невыровненные mov (например, movupd, lddqu) работают медленнее, или, возможно, они нацелены на процессоры без SSE2 / 3

11
Выравнивание адреса приводит к оптимизированному использованию кэша, а также к более высокой пропускной способности между различными уровнями кэша и оперативной памяти (для наиболее распространенных рабочих нагрузок). Смотрите здесь stackoverflow.com/questions/381244/purpose-of-memory-alignment
Deepthought

Ответы:


587

Оригинальный ответ

{
    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_allocalignmentsizealignmentsizealignment

Возвращает
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 была опцией, когда на вопрос был первоначально дан ответ.

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


13
И я зациклен на C ++, но я не очень верю, что ~ 0x0F будет правильно расширяться до размера указателя. Если этого не произойдет, весь ад вырвется на свободу, потому что вы также замаскируете самые важные биты вашего указателя. Хотя я могу ошибаться по этому поводу.
Билл К

66
Кстати, «+15» работает так же, как и «+16» ... но в этой ситуации никакого практического влияния нет.
Menkboy

15
Комментарии «+ 15» от Менкбоя и Грега верны, но malloc () почти наверняка округлил бы это до 16 в любом случае. Использование +16 немного легче объяснить. Обобщенное решение неудобно, но выполнимо.
Джонатан Леффлер

6
@Aerovistae: Это немного хитрый вопрос, и в основном он зависит от вашего понимания того, как сделать произвольное число (фактически адрес, возвращаемый распределителем памяти) определенным требованием (кратным 16). Если бы вам сказали округлить 53 до ближайшего кратного 16, как бы вы это сделали? Процесс не очень отличается для адресов; просто числа, с которыми вы обычно имеете дело, больше. Не забывайте, вопросы об интервью задают, чтобы узнать, как вы думаете, а не узнать, знаете ли вы ответ.
Джонатан Леффлер

3
@akristmann: исходный код верен, если у вас есть <inttypes.h>доступный от C99 (по крайней мере для строки формата - возможно, значения должны быть переданы с помощью приведения:) (uintptr_t)mem, (uintptr_t)ptr. Строка формата основана на конкатенации строк, а макрос PRIXPTR - это корректный указатель printf()длины и типа для шестнадцатеричного вывода uintptr_tзначения. Альтернативой является использование, %pно вывод этого зависит от платформы (некоторые добавляют начальные 0x, большинство - нет) и, как правило, пишутся шестнадцатеричными строчными буквами, что мне не нравится; то, что я написал, одинаково для разных платформ.
Джонатан Леффлер

58

Три несколько разных ответа в зависимости от того, как вы смотрите на вопрос:

1) Достаточно хорошо для точного задаваемого вопроса является решение Джонатана Леффлера, за исключением того, что для округления до 16 выровнено, вам нужно только 15 дополнительных байтов, а не 16.

A:

/* allocate a buffer with room to add 0-15 bytes to ensure 16-alignment */
void *mem = malloc(1024+15);
ASSERT(mem); // some kind of error-handling code
/* round up to multiple of 16: add 15 and then round down by masking */
void *ptr = ((char*)mem+15) & ~ (size_t)0x0F;

B:

free(mem);

2) Для более общей функции выделения памяти вызывающая сторона не хочет отслеживать два указателя (один для использования и один для освобождения). Таким образом, вы сохраняете указатель на «настоящий» буфер под выровненным буфером.

A:

void *mem = malloc(1024+15+sizeof(void*));
if (!mem) return mem;
void *ptr = ((char*)mem+sizeof(void*)+15) & ~ (size_t)0x0F;
((void**)ptr)[-1] = mem;
return ptr;

B:

if (ptr) free(((void**)ptr)[-1]);

Обратите внимание, что в отличие от (1), где в mem было добавлено только 15 байтов, этот код может фактически уменьшить выравнивание, если ваша реализация гарантирует 32-байтовое выравнивание из malloc (маловероятно, но в теории реализация C может иметь 32 байта). выровненный тип). Это не имеет значения, если все, что вы делаете, это вызываете memset_16aligned, но если вы используете память для структуры, это может иметь значение.

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

[ Добавлено : «Стандартный» трюк - создать объединение «максимально выровненных типов» для определения необходимого выравнивания. Максимально выровненные типы, вероятно, будут (в C99) ' long long', ' long double', ' void *' или ' void (*)(void)'; если вы включите <stdint.h>, вы можете предположительно использовать « intmax_t» вместо long long(и на машинах Power 6 (AIX) intmax_tвы получите 128-битный целочисленный тип). Требования выравнивания для этого объединения можно определить, внедрив его в структуру с одним символом, за которым следует объединение:

struct alignment
{
    char     c;
    union
    {
        intmax_t      imax;
        long double   ldbl;
        void         *vptr;
        void        (*fptr)(void);
    }        u;
} align_data;
size_t align = (char *)&align_data.u.imax - &align_data.c;

Затем вы будете использовать большее из запрошенного выравнивания (в примере 16) и alignзначение, рассчитанное выше.

На (64-разрядной) ОС Solaris 10 malloc()создается впечатление, что базовое выравнивание для результата кратно 32 байтам.
]

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

3) Используйте то, что предоставляет ваша платформа: posix_memalignдля POSIX, _aligned_mallocв Windows.

4) Если вы используете C11, то самый чистый - портативный и лаконичный вариант - использовать стандартную библиотечную функцию, aligned_allocкоторая была представлена ​​в этой версии спецификации языка.


1
Я согласен - я думаю, что цель вопроса в том, чтобы код, который освобождает блок памяти, имел доступ только к «приготовленному» 16-байтовому выровненному указателю.
Майкл Берр

1
Для общего решения - вы правы. Тем не менее, шаблон кода в вопросе ясно показывает оба.
Джонатан Леффлер

1
Конечно, и в хорошем интервью вы даете ответ, и если интервьюер хочет увидеть мой ответ, они меняют вопрос.
Стив Джессоп

1
Я возражаю против использования ASSERT(mem);для проверки результатов распределения; assertдля ловли ошибок программирования и нехватки ресурсов времени выполнения.
Хловдал

4
Использование двоичного кода & с a char *и a size_tприведет к ошибке. Вы должны использовать что-то вроде uintptr_t.
Марко


20

Вот альтернативный подход к части «округления». Не самое блестяще закодированное решение, но оно выполняет свою работу, и этот тип синтаксиса немного легче запомнить (плюс будет работать для значений выравнивания, которые не являются степенью 2). Приведение uintptr_tбыло необходимо, чтобы успокоить компилятор; арифметика указателей не очень любит деление или умножение.

void *mem = malloc(1024 + 15);
void *ptr = (void*) ((uintptr_t) mem + 15) / 16 * 16;
memset_16aligned(ptr, 0, 1024);
free(mem);

2
В общем, когда у вас есть «unsigned long long», у вас также есть uintptr_t, который явно определен как достаточно большой, чтобы содержать указатель данных (void *). Но ваше решение действительно имеет свои достоинства, если по какой-то причине вам требуется выравнивание, которое не было степенью 2. Маловероятно, но возможно.
Джонатан Леффлер

@Andrew: Upvoted для этого типа синтаксиса немного легче запомнить (плюс будет работать для значений выравнивания, которые не имеют степени 2) .
legends2k

19

К сожалению, в C99 кажется довольно сложно гарантировать какое-либо выравнивание способом, который был бы переносим на любую реализацию C, соответствующую C99. Почему? Поскольку указатель не гарантированно является «байтовым адресом», который можно представить с помощью плоской модели памяти. Также не гарантировано представление uintptr_t , которое в любом случае является необязательным типом.

Мы могли бы знать о некоторых реализациях, которые используют представление для void * (и по определению также char * ), который является простым байтовым адресом, но в C99 он непрозрачен для нас, программистов. Реализация может представлять указатель с помощью набора { сегмент , смещение }, где смещение может иметь выравнивание «кто знает, что» в реальности. Да, указатель может даже быть некоторой формой значения поиска в хеш-таблице или даже значением поиска в связанном списке. Это может кодировать информацию о границах.

В недавнем черновике C1X для стандарта C мы видим ключевое слово _Alignas . Это может помочь немного.

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

Было бы хорошо ошибиться в этом утверждении.


С11 имеет aligned_alloc(). (C ++ 11/14 / 1z все еще не имеет его). _Alignas()и C ++ alignas()ничего не делает для динамического размещения, только для автоматического и статического хранения (или структурного размещения).
Питер Кордес

15

На фронте заполнения счетчика 16 по 15 байтов фактическое число, которое нужно добавить, чтобы получить выравнивание N, равно max (0, NM), где M - естественное выравнивание распределителя памяти (и оба - степени 2).

Так как минимальное выравнивание памяти любого распределителя составляет 1 байт, 15 = max (0,16-1) является консервативным ответом. Однако, если вы знаете, что ваш распределитель памяти будет выдавать вам 32-битные адреса, выровненные по int (что довольно часто), вы могли бы использовать 12 в качестве пэда.

Это не важно для этого примера, но это может быть важно для встраиваемой системы с 12 КБ ОЗУ, где каждый сохраненный int имеет значение.

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

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

#define MEMORY_ALLOCATOR_NATIVE_ALIGNMENT    4
#define ALIGN_PAD2(N,M) (((N)>(M)) ? ((N)-(M)) : 0)
#define ALIGN_PAD(N) ALIGN_PAD2((N), MEMORY_ALLOCATOR_NATIVE_ALIGNMENT)

8

Возможно они были бы удовлетворены знанием memalign ? И, как отмечает Джонатан Леффлер, есть две новые предпочтительные функции, о которых нужно знать.

Ой, Флорин победил меня в этом. Однако, если вы прочитаете справочную страницу, на которую я ссылаюсь, вы, скорее всего, поймете пример, предоставленный более ранним постером.


1
Обратите внимание , что в настоящее время (февраль 2016) версия указанной странице говорит , что « memalignфункция устарела и aligned_allocили posix_memalignвместо них следует использовать». Я не знаю, что он сказал в октябре 2008 года, но, вероятно, не упомянул, aligned_alloc()как это было добавлено в C11.
Джонатан Леффлер

5

Мы делаем такие вещи постоянно для Accelerate.framework, сильно векторизованной библиотеки OS X / iOS, где мы должны постоянно уделять внимание выравниванию. Есть довольно много вариантов, один или два из которых я не видел упомянутых выше.

Самый быстрый метод для такого маленького массива - просто положить его в стек. С GCC / Clang:

 void my_func( void )
 {
     uint8_t array[1024] __attribute__ ((aligned(16)));
     ...
 }

Бесплатно () не требуется. Обычно это две инструкции: вычтите 1024 из указателя стека, затем И указатель стека с -alignment. Предположительно, запрашивающему потребовались данные в куче, потому что его срок службы массива превысил стек, или рекурсия работает, или пространство стека стоит серьезной премии.

В OS X / iOS все вызовы malloc / calloc / и т. Д. всегда выровнены по 16 байтов. Например, если вам нужно выровнять 32 байта для AVX, вы можете использовать posix_memalign:

void *buf = NULL;
int err = posix_memalign( &buf, 32 /*alignment*/, 1024 /*size*/);
if( err )
   RunInCirclesWaivingArmsWildly();
...
free(buf);

Некоторые люди упоминали интерфейс C ++, который работает аналогично.

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

Сырный: Включите охрану malloc или подобное. Буферы размером n * 16 байтов, такие как этот, будут выровнены по n * 16 байтов, потому что VM используется для перехвата, и ее границы находятся на границах страницы.

Некоторые функции Accelerate.framework используют предоставленный пользователем временный буфер для использования в качестве рабочего пространства. Здесь мы должны предположить, что переданный нам буфер сильно смещен, и пользователь активно пытается усложнить нашу жизнь. (Наши тестовые примеры прикрепляют защитную страницу прямо перед и после временного буфера, чтобы подчеркнуть злобу.) Здесь мы возвращаем минимальный размер, который нам нужен, чтобы гарантировать 16-байтовый выровненный сегмент где-то в нем, а затем вручную выравниваем буфер после. Этот размер - требуемый_размер + выравнивание - 1. Итак, в этом случае это 1024 + 16 - 1 = 1039 байт. Затем выровняйте так:

#include <stdint.h>
void My_func( uint8_t *tempBuf, ... )
{
    uint8_t *alignedBuf = (uint8_t*) 
                          (((uintptr_t) tempBuf + ((uintptr_t)alignment-1)) 
                                        & -((uintptr_t) alignment));
    ...
}

Добавление alignment-1 переместит указатель за первый выровненный адрес, а затем ANDing с -alignment (например, 0xfff ... ff0 для alignment = 16) вернет его на выровненный адрес.

Как описано в других статьях, в других операционных системах без 16-байтовых гарантий выравнивания вы можете вызывать malloc с большим размером, позже выделить указатель free (), затем выполнить выравнивание, как описано выше, и использовать выровненный указатель, так же как описано для нашего временного буфера.

Что касается align_memset, это довольно глупо. Вам нужно только зациклить до 15 байтов для достижения выровненного адреса, а затем продолжить с выровненными хранилищами с некоторым возможным кодом очистки в конце. Вы можете даже выполнить очистку битов в векторном коде, либо в виде невыровненных хранилищ, которые перекрывают выровненную область (при условии, что длина равна по крайней мере длине вектора), либо используя что-то вроде movmaskdqu. Кто-то просто ленится. Тем не менее, это, вероятно, разумный вопрос для интервью, если интервьюер хочет знать, довольны ли вы stdint.h, побитовыми операторами и основами памяти, поэтому надуманный пример можно простить.


5

Я удивлен, что никто не проголосовал за ответ Шао , что, насколько я понимаю, невозможно сделать то, что просят в стандартном C99, поскольку формальное преобразование указателя на интегральный тип - неопределенное поведение. (Помимо стандарта, разрешающего преобразование <-> , но стандарт, по-видимому, не позволяет выполнять какие-либо манипуляции со значением и затем преобразовывать его обратно.)uintptr_tvoid*uintptr_t


Не требуется, чтобы тип uintptr_t существовал или чтобы его биты имели какое-либо отношение к битам в базовом указателе. Если нужно перераспределить память, сохраните указатель как unsigned char* myptr; а затем вычислить `mptr + = (16- (uintptr_t) my_ptr) & 0x0F, поведение будет определено во всех реализациях, которые определяют my_ptr, но будет ли выровнен результирующий указатель, будет зависеть от отображения между битами uintptr_t и адресами.
суперкат

3

использование memalign, Aligned-Memory-Blocks может быть хорошим решением для этой проблемы.


Обратите внимание , что в настоящее время (февраль 2016) версия указанной странице говорит , что « memalignфункция устарела и aligned_allocили posix_memalignвместо них следует использовать». Я не знаю, что это было сказано в октябре 2010 года.
Джонатан Леффлер

3

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

Есть ли фундаментальная причина, по которой я скучаю, поскольку никто другой не предложил это?

В качестве идентификатора, поскольку я использовал массив char (предполагая, что системный char равен 8 битам (т.е. 1 байт)), я не вижу необходимости в этом __attribute__((packed))(исправьте меня, если я ошибаюсь), но я поставил его в любом случае.

Это работает на двух системах, на которых я его пробовал, но возможно, что существует оптимизация компилятора, о которой я не подозреваю, что она дает мне ложные срабатывания в отношении эффективности кода. Я использовал gcc 4.9.2на OSX и gcc 5.2.1на Ubuntu.

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

int main ()
{

   void *mem;

   void *ptr;

   // answer a) here
   struct __attribute__((packed)) s_CozyMem {
       char acSpace[16];
   };

   mem = malloc(sizeof(struct s_CozyMem));
   ptr = mem;

   // memset_16aligned(ptr, 0, 1024);

   // Check if it's aligned
   if(((unsigned long)ptr & 15) == 0) printf("Aligned to 16 bytes.\n");
   else printf("Rubbish.\n");

   // answer b) here
   free(mem);

   return 1;
}

1

Специфичные для MacOS X:

  1. Все указатели, выделенные с помощью malloc, выровнены по 16 байтов.
  2. C11 поддерживается, так что вы можете просто вызвать align_malloc (16, size).

  3. MacOS X выбирает код, оптимизированный для отдельных процессоров во время загрузки для memset, memcpy и memmove, и этот код использует трюки, о которых вы никогда не слышали, чтобы сделать его быстрым. 99% вероятности, что memset работает быстрее, чем любой рукописный memset16, что делает весь вопрос бессмысленным.

Если вы хотите 100% портативное решение, до C11 его нет. Потому что нет портативного способа проверить выравнивание указателя. Если он не должен быть на 100% портативным, вы можете использовать

char* p = malloc (size + 15);
p += (- (unsigned int) p) % 16;

Это предполагает, что выравнивание указателя сохраняется в младших битах при преобразовании указателя в unsigned int. Преобразование в unsigned int теряет информацию и определяется реализацией, но это не имеет значения, потому что мы не конвертируем результат обратно в указатель.

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


1
Где вы находите aligned_mallocв OS X? Я использую Xcode 6.1, и он нигде не определен в iOS SDK, и нигде не объявлен /usr/include/*.
Тодд Леман

То же самое для XCode 7.2 на El Capitan (Mac OS X 10.11.3). Функция C11 в любом случае есть aligned_alloc(), но она также не объявлена. Из GCC 5.3.0 я получаю интересные сообщения alig.c:7:15: error: incompatible implicit declaration of built-in function ‘aligned_alloc’ [-Werror]и alig.c:7:15: note: include ‘<stdlib.h>’ or provide a declaration of ‘aligned_alloc’. Код действительно включает в себя <stdlib.h>, но ни -std=c11ни -std=gnu11изменил сообщения об ошибках.
Джонатан Леффлер

0

Вы также можете добавить около 16 байтов и затем выровнять исходный ptr в 16 бит, добавив (16-mod), как показано под указателем:

main(){
void *mem1 = malloc(1024+16);
void *mem = ((char*)mem1)+1; // force misalign ( my computer always aligns)
printf ( " ptr = %p \n ", mem );
void *ptr = ((long)mem+16) & ~ 0x0F;
printf ( " aligned ptr = %p \n ", ptr );

printf (" ptr after adding diff mod %p (same as above ) ", (long)mem1 + (16 -((long)mem1%16)) );


free(mem1);
}

0

Если существуют ограничения, вы не можете тратить один байт, тогда это решение работает: Примечание: есть случай, когда это может выполняться бесконечно: D

   void *mem;  
   void *ptr;
try:
   mem =  malloc(1024);  
   if (mem % 16 != 0) {  
       free(mem);  
       goto try;
   }  
   ptr = mem;  
   memset_16aligned(ptr, 0, 1024);

Существует очень хороший шанс, что если вы выделите, а затем освободите блок из N байтов, а затем запросите другой блок из N байтов, исходный блок будет возвращен снова. Таким образом, бесконечный цикл очень вероятен, если первое распределение не соответствует требованию выравнивания. Конечно, это позволяет избежать потери одного байта за счет потери многих циклов ЦП.
Джонатан Леффлер

Вы уверены, что %оператор определен void*значимым образом?
Аджай Брахмакшатрия

0

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

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

C11 поддерживается, так что вы можете просто позвонить aligned_alloc (16, size).

void *mem = malloc(1024+16);
void *ptr = ((char *)mem+16) & ~ 0x0F;
memset_16aligned(ptr, 0, 1024);
free(mem);

1
Во многих 64-битных системах возвращаемый указатель malloc()действительно выровнен по 16-байтовой границе, но ничто в любом стандарте не гарантирует, что он просто будет достаточно хорошо выровнен для любого использования, а во многих 32-битных системах при выравнивании Достаточно 8-байтовой границы, а для некоторых достаточно 4-байтовой границы.
Джонатан Леффлер

0
size =1024;
alignment = 16;
aligned_size = size +(alignment -(size %  alignment));
mem = malloc(aligned_size);
memset_16aligned(mem, 0, 1024);
free(mem);

Надеюсь, что это самая простая реализация, дайте мне знать ваши комментарии.


-3
long add;   
mem = (void*)malloc(1024 +15);
add = (long)mem;
add = add - (add % 16);//align to 16 byte boundary
ptr = (whatever*)(add);

Я думаю, что есть проблема с этим, потому что ваше дополнение будет указывать на местоположение, которое не является malloc'd - Не уверен, как это сработало на вашем.
Результаты

@ Сам должен быть add += 16 - (add % 16). (2 - (2 % 16)) == 0,
СС Энн
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.