【问题标题】:Hinnant's short_alloc and alignment guaranteesHinnant 的 short_alloc 和对齐保证
【发布时间】:2016-02-16 19:33:54
【问题描述】:

我最近遇到了 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 的倍数。

但在更改任何内容之前,我想确保我在这里没有遗漏任何东西......

【问题讨论】:

标签: c++ c++11 memory-management


【解决方案1】:

这段代码是在我的工具箱中有std::max_align_t(现在位于&lt;cstddef&gt;)之前编写的。我现在可以这样写:

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

在我的系统上,它与当前代码完全相同,但现在更便携。这是newmalloc 保证返回的对齐方式。一旦你有了这个“最大对齐”的缓冲区,你就可以在其中放入任何一种类型的数组。但是您不能对不同的类型使用相同的arena(至少不能使用具有不同对齐要求的不同类型)。出于这个原因,也许最好在第二个size_t 上模板arena,它等于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&lt;unsigned char&gt;),可以简单地适当调整alignment。还可以让short_alloc::allocatealignof(T) 传递给arena::allocateassert(requested_align &lt;= 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,您并没有向下调整太多。

再次更新!

我已经更新了这个分配器的descriptioncode,因为这个很好的问题(我多年来一直忽略这个代码)。

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

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

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

【讨论】:

  • @HowardHinnant 请将许可证信息添加到您的代码中,以便安全使用。大多数项目禁止使用未经许可的代码,即使是开源项目。
  • 疯狂的想法:模板化实现能否自动选择正确的对齐方式(即最小的 2 次幂),基于 SFINAE 和类似 stackoverflow.com/questions/49079196/… 的东西?即:尝试alignment=1,然后如果THIS_CODE_DOES_NOT_COMPILE(...一些创建容器并插入项目的示例代码...),然后尝试(通过模板递归)alignment=alignment*2 等递归基本情况设置为 alignment=alignof(std::max_align_t)。
  • 建议让“这个分配器比以往任何时候都更容易使用”部分更容易:template&lt;typename WhatIHas&gt; struct HasA { WhatIHas what_i_has; }; template&lt;typename Container&gt; struct ShortContainer : private HasA&lt;typename Container::allocator_type::arena_type&gt;, public Container { ShortContainer(): Container(this-&gt;what_i_has) {} }; template&lt;typename T, size_t BufSizeBytes = 200&gt; using ShortVector = ShortContainer&lt;std::vector&lt;T, short_alloc&lt;T, BufSizeBytes, alignof(T)&gt;&gt;&gt;; 然后它可以简单地使用ShortVector&lt;int&gt; v;ShortVector&lt;int, 1000&gt; v;
  • 另外,值得注意的是:当将 std::vector 与此分配器一起使用时,应立即始终调用 reserve(BufSizeBytes/sizeof(T)),以避免意外的静默溢出到堆。如果我们使用更简单的 short_alloc 变体,这种变体更容易出错,而不是溢出到堆中,这一点就会变得很明显。 SmallVector 类型可能应该在其 ctor 中自动执行这样的 reserve() (但不幸的是,这可能会使这个答案的阐述有点混乱,所以也许只是在最后简单地指出这一点)。
  • 对 SmallVector 示例的另一个建议:当我看到 SmallVector&lt;int, 1000&gt; 时,我立即认为它在谈论 1000 个整数;我(也许还有其他人?)发现它实际上是在谈论 1000/sizeof(int) 个整数,这让我感到惊讶。如果该模板参数是 MaxItems 而不是 BufSizeBytes,也许就不会那么令人惊讶了?
猜你喜欢
  • 2019-06-02
  • 2013-02-15
  • 2013-10-14
  • 2015-04-02
  • 2017-06-20
  • 1970-01-01
  • 1970-01-01
  • 2016-08-16
  • 1970-01-01
相关资源
最近更新 更多