Как выделить выровненную память только с использованием стандартной библиотеки - PullRequest
396 голосов
/ 23 октября 2008

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

Функция memset_16aligned требует 16-байтового выровненного указателя, переданного ей, иначе произойдет сбой.

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

{    
   void *mem;
   void *ptr;

   // answer a) here

   memset_16aligned(ptr, 0, 1024);

   // answer b) here    
}

Ответы [ 17 ]

559 голосов
/ 23 октября 2008

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

{
    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() должен возвращать указатель, который достаточно хорошо выровнен для любой цели. Однако значение 'any' в первую очередь относится к таким вещам, как базовые типы - 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 , которое повторяло точку, впервые высказанную Биллом K несколько лет назад, которую мне удалось пропустить до сих пор. ]

#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() в функцию распределения общего назначения, единственное возвращаемое значение от распределителя должно было бы кодировать адрес освобождения, как указали несколько человек в своих ответах.

Проблемы с интервьюерами

Uri прокомментировал: Может быть, у меня сегодня утром проблема с пониманием прочитанного, но если вопрос об интервью конкретно говорит: «Как бы вы распределили 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.

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

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

56 голосов
/ 23 октября 2008

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

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, которая была введена в этой версии спецификации языка.

37 голосов
/ 23 октября 2008

Вы также можете попробовать posix_memalign() (на платформах POSIX, конечно).

19 голосов
/ 23 октября 2008

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

void *mem = malloc(1024 + 15);
void *ptr = (void*) ((uintptr_t) mem + 15) / 16 * 16;
memset_16aligned(ptr, 0, 1024);
free(mem);
18 голосов
/ 07 августа 2010

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

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

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

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

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

15 голосов
/ 21 октября 2009

На фронте заполнения 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 голосов
/ 23 октября 2008

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

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

5 голосов
/ 14 июля 2011

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

5 голосов
/ 05 июня 2014

Мы делаем такие вещи постоянно для 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 / etc. всегда выровнены по 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 для align = 16) вернет его к выровненному адресу.

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

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

3 голосов
/ 11 мая 2016

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

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

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

Это работает на двух системах, на которых я его пробовал, но возможно, что существует оптимизация компилятора, о которой я не подозреваю, что я получаю ложные срабатывания в отношении эффективности кода. Я использовал 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;
}
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...