Подтвердить что ты не робот

Hinnant short_alloc и гарантии выравнивания

Недавно я встретил Howard Hinnant short_alloc, и это самый лучший пример пользовательских распределителей, которые я видел.

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

template <std::size_t N>
class arena
{
  static const std::size_t alignment = 16;
  alignas(alignment) char buf_[N];
  char* ptr_;
  //...
};

template <std::size_t N>
char*
arena<N>::allocate(std::size_t n)
{
  assert(pointer_in_buffer(ptr_) && "short_alloc has outlived arena");
  if (buf_ + N - ptr_ >= n)
  {
    char* r = ptr_;
    ptr_ += n;
    return r;
  }
  return static_cast<char*>(::operator new(n));
}

Я могу придумать несколько способов исправить это (ценой потери памяти), проще всего округлить size в функции allocate/deallocate до кратного alignment.

Но прежде чем что-либо менять, я хотел бы удостовериться, что здесь ничего не пропало...

4b9b3361

Ответ 1

Этот код был написан до того, как у меня был std::max_align_t в моем ящике инструментов (который теперь живет в <cstddef>). Я бы написал это как:

static const std::size_t alignment = alignof(std::max_align_t);

который в моей системе точно эквивалентен текущему коду, но теперь более портативен. Это выравнивание, которое new и malloc гарантированно возвращаются. И как только у вас есть этот "максимально выравниваемый" буфер, вы можете поместить в него массив любого типа. Но вы не можете использовать те же arena для разных типов (по крайней мере, не разные типы, которые имеют разные требования к выравниванию). И по этой причине, возможно, было бы лучше шаблон arena на второй size_t, который равен alignof(T). Таким образом, вы можете предотвратить случайное использование тех же arena типов с разными требованиями к выравниванию:

arena<N, alignof(T)>& a_;

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

например. на моей системе alignof(std::max_align_t) == 16. Буфер с этим выравниванием может содержать массивы:

  • Типы с alignof == 1.
  • Типы с alignof == 2.
  • Типы с alignof == 4.
  • Типы с alignof == 8.
  • Типы с alignof == 16.

Поскольку некоторые среды могут поддерживать типы, которые имеют требования "супер-выравнивания", добавленная мера безопасности должна заключаться в добавлении (скажем, в short_alloc):

static_assert(alignof(T) <= alignof(std::max_align_t), "");

Если вы супер параноик, вы также можете проверить, что alignof(T) является степенью 2, хотя сам стандарт С++ гарантирует, что это всегда будет true ([basic.align]/p4).

Обновление

Я более подробно рассмотрел эту проблему и считаю, что наилучшим решением является округление запрошенного размера размещения до следующего alignment (как предложено OP). Я обновил "short_alloc" , чтобы сделать это на моем веб-сайте.

template <std::size_t N>
char*
arena<N>::allocate(std::size_t n)
{
    assert(pointer_in_buffer(ptr_) && "short_alloc has outlived arena");
    n = align_up(n);
    if (buf_ + N - ptr_ >= n)
    {
        char* r = ptr_;
        ptr_ += n;
        return r;
    }
    return static_cast<char*>(::operator new(n));
}

В особых ситуациях, когда вы знаете, что вам не нужны максимально выровненные распределения (например, vector<unsigned char>), можно просто настроить alignment соответствующим образом. И можно также short_alloc::allocate пройти alignof(T) до arena::allocate и assert(requested_align <= alignment)

template <std::size_t N>
char*
arena<N>::allocate(std::size_t n, std::size_t requested_align)
{
    assert(requested_align <= alignment);
    assert(pointer_in_buffer(ptr_) && "short_alloc has outlived arena");
    n = align_up(n);
    if (buf_ + N - ptr_ >= n)
    {
        char* r = ptr_;
        ptr_ += n;
        return r;
    }
    return static_cast<char*>(::operator new(n));
}

Это даст вам уверенность, что если вы скорректировали alignment вниз, вы не отрегулировали его слишком далеко вниз.

Обновить снова!

Я обновил описание и код этого распределителя совсем немного из-за этого прекрасного вопроса (Я не обращал внимания на этот код в течение многих лет).

Проверка выравнивания, упомянутая в предыдущем обновлении, теперь выполняется во время компиляции (ошибки времени компиляции всегда превосходят ошибки во время выполнения, даже утверждают).

Оба параметра arena и short_alloc теперь настроены на выравнивание, чтобы вы могли легко настроить требования к выравниванию, которые вы ожидаете (и если вы догадываетесь, что это слишком мало, он попадает во время компиляции). По умолчанию этот параметр шаблона равен alignof(std::max_align_t).

Теперь функция arena::allocate выглядит следующим образом:

template <std::size_t N, std::size_t alignment>
template <std::size_t ReqAlign>
char*
arena<N, alignment>::allocate(std::size_t n)
{
    static_assert(ReqAlign <= alignment, "alignment is too small for this arena");
    assert(pointer_in_buffer(ptr_) && "short_alloc has outlived arena");
    auto const aligned_n = align_up(n);
    if (buf_ + N - ptr_ >= aligned_n)
    {
        char* r = ptr_;
        ptr_ += aligned_n;
        return r;
    }
    return static_cast<char*>(::operator new(n));
}

Благодаря шаблонам псевдонимов этот распределитель проще в использовании, чем когда-либо. Например:

// Create a vector<T> template with a small buffer of 200 bytes.
//   Note for vector it is possible to reduce the alignment requirements
//   down to alignof(T) because vector doesn't allocate anything but T's.
//   And if we're wrong about that guess, it is a comple-time error, not
//   a run time error.
template <class T, std::size_t BufSize = 200>
using SmallVector = std::vector<T, short_alloc<T, BufSize, alignof(T)>>;

// Create the stack-based arena from which to allocate
SmallVector<int>::allocator_type::arena_type a;
// Create the vector which uses that arena.
SmallVector<int> v{a};

Это не обязательно последнее слово в таких распределителях. Но, надеюсь, это прочная основа, на которой вы можете создавать свои собственные распределители.