Реализация malloc: проверка правильности выравнивания распределения

мой последний тест провален, но я думаю, что нашел решение, как его пройти. он предполагает использование alignof(max_align_t) (или даже alignof(long double)), но я не совсем уверен, правильно ли я понимаю выравнивание распределения.

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

если я прав, то почему последний тест не проходит? в противном случае разъяснения были бы оценены!

void *malloc(size_t sz, const char *file, int line) {
  (void)file, (void)line; // avoid uninitialized variable warnings
  if (default_buffer.pos + sz > default_buffer.size) {
    // Not enough space left in default buffer for allocation
    gstats.nfail++;
    gstats.fail_size += sz;
    return nullptr;
  }
  // Here i'm checking for correct allocation alignement
  if (default_buffer.pos % sz != 0) {
    default_buffer.pos += default_buffer.pos % sz;
  }
  // Otherwise there is enough space; claim the next `sz` bytes
  void *ptr = &default_buffer.buffer[default_buffer.pos];

  default_buffer.pos += sz;
  gstats.ntotal++;
  gstats.total_size += sz;
  return ptr;
}

test.c

int main() {
double* ptr = (double*) malloc(sizeof(double));
assert((uintptr_t) ptr % alignof(double) == 0);
assert((uintptr_t) ptr % alignof(unsigned long long) == 0);
assert((uintptr_t) ptr % alignof(std::max_align_t) == 0);

    char* ptr2 = (char*) malloc(1);
    assert((uintptr_t) ptr2 % alignof(double) == 0);
    assert((uintptr_t) ptr2 % alignof(unsigned long long) == 0);
    assert((uintptr_t) ptr2 % alignof(std::max_align_t) == 0);
    
    free(ptr);
    free(ptr2);

}

Я уже решил проблему и хочу понять, почему альтернатива моему решению не работает на конкретном тесте. По сути, я стремлюсь убедиться в правильности моего понимания!

Вы не передаете никакой информации о типе в свой «malloc»; просто размер. Ваш код предполагает, что «alignof» — это то же самое, что «sizeof», но это часто не так.

Den-Jason 25.03.2024 23:45
pos += pos % sz; не совпадает с sz. Вам нужно сделать pos = (pos + sz - 1) & ~(sz - 1);, предполагая, что sz является степенью 2. Это гарантирует только выравнивание pos как индекса в вашем буфере. Он не учитывает собственное выравнивание буфера. Но если вы выровняли свой буфер по sizeof(max_align_t), тогда все будет в порядке.
paddy 25.03.2024 23:53

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

paddy 25.03.2024 23:56

@paddy, так что, если sz является степенью 2, то мне нужно вычислить для него выравнивание, а в противном случае мне следует найти ближайшую степень двойки и использовать ее для выравнивания?

Grainme 26.03.2024 00:04

Да, наивный способ — начать с 1 и продолжать сдвигать влево, пока не достигнете sizeof(max_align_t) или не станет равным/превысит размер выделения. Более разумный способ — вычислить log-base-2 вашего размера и обрезать его до максимального выравнивания, что, вероятно, будет быстрее.

paddy 26.03.2024 00:07

@paddy: Используйте _Alignof (max_align_t), а не sizeof(max_align_t). Кроме того, использование пробела между _Alignof или sizeof и его операндом в круглых скобках может помочь напомнить программистам, что это оператор, а не функция, а вещь после него — это имя типа в скобках, а не список аргументов.

Eric Postpischil 26.03.2024 00:59

@paddy: Re «… продолжайте сдвигать влево, пока не достигнете… или не станет равным/превысите размер выделения»: выравнивание объекта не превышает наибольшую степень двойки, которая делит его размер. Например, если размер объекта составляет 12 байт, его требование к выравниванию составляет не более 4 байтов. Нет необходимости переходить к 16. Наибольшую степень двойки, которая делит size, можно рассчитать как size & -size.

Eric Postpischil 26.03.2024 01:04

Почему в вопросе с пометкой C есть файл с именем test.cc? .cc предназначен для исходных файлов C++, а в C++ действуют другие правила, чем в C.

Eric Postpischil 26.03.2024 01:07

Ого, это крутой трюк. Спасибо также за разъяснение требований к выравниванию. Это имеет гораздо больше смысла. Я обновлю ответ соответственно.

paddy 26.03.2024 01:09

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

Eric Postpischil 26.03.2024 01:10

я изменил название своей функции, чтобы прояснить свой вопрос

Grainme 26.03.2024 02:22
Стоит ли изучать PHP в 2026-2027 годах?
Стоит ли изучать PHP в 2026-2027 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
1
11
76
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

Как я уже упоминал в комментариях, ваш расчет выравнивания неверен. Представьте, что вам нужно выравнивание 4, а остаток по модулю равен 3. Вы не будете добавлять еще 3 — вы добавите 4 минус 3.

Вместо использования по модулю типичный способ выравнивания значения — добавить на единицу меньше, чем выравнивание (чтобы все, кроме нуля, гарантированно приводило к адресу на следующей границе выравнивания), а затем замаскировать все биты ниже, чем выравнивание:

pos = (pos + align - 1) & ~(align - 1);

Обратите внимание, что align должно быть беззнаковой степенью 2.

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

По поводу автоматического выравнивания пользователь Эрик Постпишиль также прокомментировал:

Выравнивание объекта не превышает наибольшую степень двойки, делящую его размер. Например, если размер объекта составляет 12 байт, его требование к выравниванию составляет не более 4 байтов. Нет необходимости переходить к 16. Наибольшую степень двойки, которая делит size, можно рассчитать как size & -size.

Итак, сложив все это вместе, вот простой способ выравнивания по произвольному адресу:

#include <stddef.h>
#include <stdint.h>

size_t max_align(size_t size)
{
    size_t alignment = size & -size;
    return ((alignment - 1) & (sizeof(max_align_t) - 1)) + 1;
}

void* align(void* ptr, size_t alignment)
{
    return (void*)(((uintptr_t)ptr + alignment - 1) & ~(alignment - 1));
}

Обратите внимание на грязный трюк с обрезкой до sizeof(max_align_t). Это может быть или не быть лучше, чем простое введение ветки. Я не такой уж и грубый!

Посмотрите это в действии:

void test(char* buffer, size_t size)
{
    size_t alignment = max_align(size);
    char* aligned = align(buffer, alignment);
    size_t offset = aligned - buffer;
    printf("-align=%p size=%zu alignment=%zu offset=%zu\n",
        aligned, size, alignment, offset);
}

struct foo {
    int a;
    char b[2];
    int c;
    void* d;
};

struct __attribute((packed)) bar {
    int a;
    char b[2];
    int c;
    void* d;
};

int main(void)
{
    char buffer[256];
    for (size_t offset = 0; offset < 64; offset += 13) {
        char* base = buffer + offset;
        printf("\nbuffer=%p :\n", base);
        test(base, sizeof(struct foo));
        test(base, sizeof(struct bar));
    }
}

Пример вывода:


buffer=0x7ffc98db0770 :
-align=0x7ffc98db0770 size=24 alignment=8 offset=0
-align=0x7ffc98db0770 size=18 alignment=2 offset=0

buffer=0x7ffc98db077d :
-align=0x7ffc98db0780 size=24 alignment=8 offset=3
-align=0x7ffc98db077e size=18 alignment=2 offset=1

buffer=0x7ffc98db078a :
-align=0x7ffc98db0790 size=24 alignment=8 offset=6
-align=0x7ffc98db078a size=18 alignment=2 offset=0

buffer=0x7ffc98db0797 :
-align=0x7ffc98db0798 size=24 alignment=8 offset=1
-align=0x7ffc98db0798 size=18 alignment=2 offset=1

buffer=0x7ffc98db07a4 :
-align=0x7ffc98db07a8 size=24 alignment=8 offset=4
-align=0x7ffc98db07a4 size=18 alignment=2 offset=0

Как вы упомянули в комментариях, вы, вероятно, могли бы использовать здесь что-то вроде _BitScanReverse64 (msvc) или __builtin_clzll (gcc) вместо цикла while.

Den-Jason 26.03.2024 01:31

Оказывается, в этом нет необходимости, потому что мой подход к выравниванию по размеру объекта был неправильным. Я отредактировал соответственно. Обратите внимание, что расчет выравнивания может привести к завышению оценки выравнивания. В моем примере кода max_align(sizeof(struct bar)) возвращает 2, но встроенный alignof(struct bar) на самом деле возвращает 1, потому что это упакованная структура.

paddy 26.03.2024 01:59

Другие вопросы по теме