Як розподілити вирівняну пам'ять лише за допомогою стандартної бібліотеки?


421

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

memset_16alignedФункція вимагає 16 байт , вирівняний покажчик , переданий йому, або це буде крах.

а) Як би ви виділили 1024 байти пам'яті та вирівняли її до 16-байтної межі?
b) Звільніть пам'ять після memset_16alignedвиконання.

{    
   void *mem;
   void *ptr;

   // answer a) here

   memset_16aligned(ptr, 0, 1024);

   // answer b) here    
}

89
хммм ... для довгострокової життєздатності коду, як щодо "Пожежа, хто написав memset_16 вирівняний, виправити його або замінити, щоб він не мав особливої ​​межової умови"
Стівен А. Лоу,

29
Безумовно, справедливе запитання - "чому своєрідне вирівнювання пам'яті". Але для цього можуть бути вагомі причини - в цьому випадку могло бути так, що memset_16aligned () може використовувати 128-бітні цілі числа, а це простіше, якщо пам'ять, як відомо, вирівнюється. І т. Д.
Джонатан Леффлер

5
Кожен, хто написав мемсет, може використовувати внутрішнє 16-байтове вирівнювання для очищення внутрішньої петлі та невеликий пролог / епілог даних для очищення нерівневих кінців. Це було б набагато простіше, ніж змусити кодери обробляти додаткові вказівники пам'яті.
Адісак

8
Чому хтось хоче, щоб дані вирівняні до 16-байтної межі? Ймовірно, щоб завантажити його в 128-бітні регістри SSE. Я вважаю, що (новіші) нерівні мови (наприклад, movupd, lddqu) повільніші, або, можливо, вони націлені на процесори без SSE2 / 3

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

Відповіді:


585

Оригінальна відповідь

{
    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., І покажчики на об'єкти і покажчики на функцію Коли ви роблячи більш спеціалізовані речі, наприклад, граючи з графічними системами, їм може знадобитися більш жорстке вирівнювання, ніж решта системи - отже, такі питання та відповіді.)

Наступним кроком є ​​перетворення покажчика недійсності на покажчик char; Незважаючи на 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
BTW "+15" працює так само, як "+16" ... однак практичного впливу в цій ситуації немає.
Менкбой

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), де до пам’яті додано лише 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призначений для уловлювання помилок програмування, а не для нестачі ресурсів часу виконання.
hlovdal

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
Загалом, там, де у вас "довгий без підпису", ви також маєте uintptr_t, який явно визначено, щоб бути досить великим, щоб містити покажчик даних (void *). Але у вашому рішенні справді є заслуги, якщо з якихось причин вам знадобилося вирівнювання, яке не було потужністю 2. Навряд чи, але можливо.
Джонатан Леффлер

@Andrew: Запропонований для цього типу синтаксис запам'ятовується трохи простіше (плюс буде працювати для значень вирівнювання, які не мають сили 2) .
legends2k

19

На жаль, у C99 здається досить важким гарантувати вирівнювання будь-якого виду таким чином, який би переносився для будь-якої реалізації C, що відповідає C99. Чому? Тому що вказівник не гарантовано є "байт-адресою", яку можна уявити з плоскою моделлю пам'яті. Ні представлення uintptr_t не є настільки гарантованим, що все-таки є необов'язковим типом.

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

У недавньому проекті C1X для стандарту C ми бачимо ключове слово _Alignas . Це може трохи допомогти.

Єдина гарантія, яку дає нам C99, - це те, що функції розподілу пам'яті повернуть вказівник, придатний для присвоєння покажчику, що вказує на будь-який тип об'єкта. Оскільки ми не можемо вказати вирівнювання об'єктів, ми не можемо реалізувати власні функції розподілу, відповідальні за вирівнювання у чітко визначеному, портативному порядку.

Добре було б помилитися з приводу цієї претензії.


C11 має 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 Кб оперативної пам’яті, де кожен вкладений збережений рахунок зараховується.

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

У наведеному нижче прикладі для більшості систем значення 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

Можливо, вони були б задоволені знаннями про мемалінг ? І як зазначає Джонатан Леффлер, слід знати дві новіші переважні функції.

На жаль, флорин бив мене до цього. Однак якщо ви прочитаєте чоловічу сторінку, на яку я пов’язаний, ви, швидше за все, зрозумієте приклад, поданий раніше плакатом.


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

У 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 () має перевагу в тому, що буфер можна виділити попередньо ініціалізованим, якщо в ньому є щось не нульове. Оскільки вони мають розмір вирівнювання сторінки, ви не отримаєте мінімального розміщення від них, і, ймовірно, виникла помилка VM при першому натисканні на нього.

Сирний: увімкніть захисний лоток або подібний. Буфери розміром з 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));
    ...
}

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

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

Що стосується align_memset, то це досить нерозумно. Вам потрібно лише зафіксувати до 15 байт, щоб досягти вирівняної адреси, а потім продовжити вирівнювання магазинів після цього з можливим кодом очищення в кінці. Можна навіть робити біти очищення у векторному коді, або як нестандартні сховища, які перекривають вирівняну область (за умови, що довжина не менше довжини вектора), або використовуючи щось на зразок movmaskdqu. Хтось просто ледачий. Однак, мабуть, є розумним питанням інтерв'ю, якщо інтерв'юер хоче знати, чи вам подобаються stdint.h, бітові оператори та основи пам'яті, тому надуманий приклад можна пробачити.


5

Я здивований , НІХТО проголосували до Шао «s відповідь , що, як я розумію, що це неможливо зробити те , що просив у стандарті C99, так як перетворення покажчик на цілочисельний тип формально не визначено поведінку. (Крім стандарту, що дозволяє перетворити uintptr_t<-> void*, але здається, що стандарт не дозволяє робити будь-які маніпуляції зі uintptr_tзначенням, а потім перетворювати його назад.)


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

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 підтримується, тож ви можете просто зателефонувати в посилання_malloc (16, розмір).

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

Якщо ви хочете 100% портативного рішення, перед C11 його немає. Тому що немає портативного способу перевірити вирівнювання вказівника. Якщо він не повинен бути 100% портативним, ви можете використовувати

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

Це передбачає, що вирівнювання вказівника зберігається в найнижчих бітах при перетворенні вказівника на непідписаний int. Перетворення в непідписаний int втрачає інформацію і визначається реалізацією, але це не має значення, оскільки ми не конвертуємо результат назад у покажчик.

Жахлива частина, звичайно, полягає в тому, що оригінальний вказівник потрібно зберегти десь, щоб зателефонувати безкоштовно (). Так що в цілому я б дуже сумнівався в мудрості цього дизайну.


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-мод), як нижче вказівника:

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 - Не знаєте, як це працювало на вашому.
Результати шляху

@Sam Це повинно бути add += 16 - (add % 16). (2 - (2 % 16)) == 0.
СС Енн
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.