欣南特的short_alloc和对齐保证

Hinnant's short_alloc and alignment guarantees

本文关键字:对齐 alloc short      更新时间:2023-10-16

我最近遇到了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));
}

我可以想出几种方法来解决这个问题(以一些内存浪费为代价),最简单的方法是将allocate/deallocate函数中的size四舍五入为alignment的倍数。

但在改变任何事情之前,我想确保我没有错过什么。。。

这段代码是在我的工具箱(现在位于<cstddef>中)中有std::max_align_t之前编写的。我现在把它写成:

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

在我的系统上,它与当前代码完全等效,但现在更具可移植性。这是newmalloc保证返回的对准。一旦你有了这个"最大对齐"的缓冲区,你就可以在其中放入任何一种类型的数组。但你不能对不同的类型使用相同的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的幂,尽管C++标准本身保证这将永远是真的([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::allocateassert(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,你不会把它向下调整太远。

再次更新

由于这个极好的问题(我多年来一直忽略这个代码),我已经更新了这个分配器的描述和代码。

上一次更新中提到的对齐检查现在在编译时完成(编译时错误总是优于运行时错误,甚至断言)。

arenashort_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};

这不一定是此类分配器的最终决定。但希望这是一个坚实的基础,您可以在此基础上构建自定义分配器。