【问题标题】:Stack-buffer based STL allocator?基于堆栈缓冲区的 STL 分配器?
【发布时间】:2011-12-24 09:07:39
【问题描述】:

我想知道是否有一个符合 C++ 标准库的 allocator,它使用位于堆栈上的(固定大小的)缓冲区。

不知何故,这个问题似乎还没有在 SO 上以这种方式提出,尽管它可能在其他地方已经隐含地回答了。

所以基本上,就我的搜索而言,似乎应该可以创建一个使用固定大小缓冲区的分配器。现在,乍一看,这应该意味着它应该有可能有一个使用固定大小缓冲区的分配器“存在”在堆栈上,但它确实出现,周围没有广泛的这种实现。

让我举个例子来说明我的意思:

{ ...
  char buf[512];
  typedef ...hmm?... local_allocator; // should use buf
  typedef std::basic_string<char, std::char_traits<char>, local_allocator> lstring;
  lstring str; // string object of max. 512 char
}

这将如何实现?


answer to this other question(感谢 R. Martinho Fernandes)链接到来自铬源的基于堆栈的分配器:http://src.chromium.org/viewvc/chrome/trunk/src/base/stack_container.h

然而,这个类似乎非常奇特,尤其是因为这个 StackAllocator 没有默认的 ctor —— 我在想 every allocator class needs a default ctor

【问题讨论】:

  • @R.MartinhoFernandes - 想要吗? Weeeel,因为这意味着没有堆分配(没有调用 gobal new)并且缓冲区将非常本地化。我不打算在我的代码中到处撒那个东西,但我想知道它是否实际上可行。
  • 这个也可能有帮助:stackoverflow.com/questions/354442/… 请务必阅读警告。 IMO 这种事情带来的麻烦多于其价值。
  • 我认为基于堆栈的分配器会破坏移动操作
  • @ggg - 仅当您移出当前范围时。不仅移动,它还会破坏旧的 swap 等。通过基于堆栈的分配器分配的 Nothing 必须离开本地范围,只要与基于堆栈缓冲区的分配器相关联的任何东西的生命周期是在分配器被销毁之前结束,一切都很好。

标签: c++ stl stack allocator


【解决方案1】:

从 c++17 开始,它实际上非常简单。 完全归功于the dumbest allocator 的作者,因为这就是它的基础。

最愚蠢的分配器是一个单一的凹凸分配器,它将char[] 资源作为其底层存储。在原始版本中,char[] 通过mmap 放置在堆上,但将其更改为指向堆栈上的char[] 是微不足道的。

template<std::size_t Size=256>                                                                                                                               
class bumping_memory_resource {                                                                                                                              
  public:                                                                                                                                                    
  char buffer[Size];                                                                                                                                         
  char* _ptr;                                                                                                                                                

  explicit bumping_memory_resource()                                                                                                                         
    : _ptr(&buffer[0]) {}                                                                                                                                    

  void* allocate(std::size_t size) noexcept {                                                                                                                
    auto ret = _ptr;                                                                                                                                         
    _ptr += size;                                                                                                                                            
    return ret;                                                                                                                                              
  }                                                                                                                                                          

  void deallocate(void*) noexcept {}                                                                                                                         
};                                                                                                                                                           

这会在创建时在堆栈上分配Size 字节,默认为256

template <typename T, typename Resource=bumping_memory_resource<256>>                                                                                        
class bumping_allocator {                                                                                                                                    
  Resource* _res;                                                                                                                                            

  public:                                                                                                                                                    
  using value_type = T;                                                                                                                                      

  explicit bumping_allocator(Resource& res)                                                                                                                  
    : _res(&res) {}                                                                                                                                          

  bumping_allocator(const bumping_allocator&) = default;                                                                                                     
  template <typename U>                                                                                                                                      
  bumping_allocator(const bumping_allocator<U,Resource>& other)                                                                                              
    : bumping_allocator(other.resource()) {}                                                                                                                 

  Resource& resource() const { return *_res; }                                                                                                               

  T*   allocate(std::size_t n) { return static_cast<T*>(_res->allocate(sizeof(T) * n)); }                                                                    
  void deallocate(T* ptr, std::size_t) { _res->deallocate(ptr); }                                                                                            

  friend bool operator==(const bumping_allocator& lhs, const bumping_allocator& rhs) {                                                                       
    return lhs._res == rhs._res;                                                                                                                             
  }                                                                                                                                                          

  friend bool operator!=(const bumping_allocator& lhs, const bumping_allocator& rhs) {                                                                       
    return lhs._res != rhs._res;                                                                                                                             
  }                                                                                                                                                          
};                                                                                                                                                           

这是真正的分配器。请注意,将重置添加到资源管理器将是微不足道的,让您再次从区域的开头创建一个新的分配器。也可以实现一个环形缓冲区,但会带来所有常见的风险。

至于你什么时候可能想要这样的东西:我在嵌入式系统中使用它。嵌入式系统通常对堆碎片反应不佳,因此能够使用不在堆上的动态分配有时会很方便。

【讨论】:

    【解决方案2】:

    显然,is a conforming Stack Allocator 来自一个Howard Hinnant

    它通过使用固定大小的缓冲区(通过引用的arena 对象)工作,如果请求的空间过多,则回退到堆中。

    这个分配器没有默认的ctor,因为霍华德说:

    我用完全符合 C++11 的新分配器更新了这篇文章。

    我想说分配器不需要默认 ctor。

    【讨论】:

    • 它不可能符合 C++,因为没有符合标准的方法来确定给定指针是指向堆栈缓冲区内部还是外部。
    • 嗯,所以问题是两个不指向同一个内存块的指针的比较(即&lt;&gt;&lt;=&gt;=)不是由标准,但堆栈分配器说 bool pointer_in_buffer(char* p) noexcept {return buf_ &lt;= p &amp;&amp; p &lt;= buf_ + N;}... 虽然,想想看,因为 std::less 产生 total 订单,而不仅仅是 partial 订单,我可能不得不提前撤回我的评论——使用这些可能真的有效。我忘记了这些的顺序是全部的。但是,无论如何,当前代码是不可移植的。
    • @Mehrdad - 似乎总结得差不多了,感谢您考虑这个:-)
    • 实际上,我又开始怀疑这一点了。订单是全部的,但不能保证它是严格的,是吗?在这种情况下 a
    【解决方案3】:

    绝对可以创建一个完全符合 C++11/C++14 的堆栈分配器*。但是您需要考虑有关堆栈分配的实现和语义以及它们如何与标准容器交互的一些后果。

    这是一个完全符合 C++11/C++14 的堆栈分配器(也托管在我的 github 上):

    #include <functional>
    #include <memory>
    
    template <class T, std::size_t N, class Allocator = std::allocator<T>>
    class stack_allocator
    {
        public:
    
        typedef typename std::allocator_traits<Allocator>::value_type value_type;
        typedef typename std::allocator_traits<Allocator>::pointer pointer;
        typedef typename std::allocator_traits<Allocator>::const_pointer const_pointer;
        typedef typename Allocator::reference reference;
        typedef typename Allocator::const_reference const_reference;
        typedef typename std::allocator_traits<Allocator>::size_type size_type;
        typedef typename std::allocator_traits<Allocator>::difference_type difference_type;
    
        typedef typename std::allocator_traits<Allocator>::const_void_pointer const_void_pointer;
        typedef Allocator allocator_type;
    
        public:
    
        explicit stack_allocator(const allocator_type& alloc = allocator_type()) 
            : m_allocator(alloc), m_begin(nullptr), m_end(nullptr), m_stack_pointer(nullptr)
        { }
    
        explicit stack_allocator(pointer buffer, const allocator_type& alloc = allocator_type())
            : m_allocator(alloc), m_begin(buffer), m_end(buffer + N), 
                m_stack_pointer(buffer)
        { }
    
        template <class U>
        stack_allocator(const stack_allocator<U, N, Allocator>& other)
            : m_allocator(other.m_allocator), m_begin(other.m_begin), m_end(other.m_end),
                m_stack_pointer(other.m_stack_pointer)
        { }
    
        constexpr static size_type capacity()
        {
            return N;
        }
    
        pointer allocate(size_type n, const_void_pointer hint = const_void_pointer())
        {
            if (n <= size_type(std::distance(m_stack_pointer, m_end)))
            {
                pointer result = m_stack_pointer;
                m_stack_pointer += n;
                return result;
            }
    
            return m_allocator.allocate(n, hint);
        }
    
        void deallocate(pointer p, size_type n)
        {
            if (pointer_to_internal_buffer(p))
            {
                m_stack_pointer -= n;
            }
            else m_allocator.deallocate(p, n);  
        }
    
        size_type max_size() const noexcept
        {
            return m_allocator.max_size();
        }
    
        template <class U, class... Args>
        void construct(U* p, Args&&... args)
        {
            m_allocator.construct(p, std::forward<Args>(args)...);
        }
    
        template <class U>
        void destroy(U* p)
        {
            m_allocator.destroy(p);
        }
    
        pointer address(reference x) const noexcept
        {
            if (pointer_to_internal_buffer(std::addressof(x)))
            {
                return std::addressof(x);
            }
    
            return m_allocator.address(x);
        }
    
        const_pointer address(const_reference x) const noexcept
        {
            if (pointer_to_internal_buffer(std::addressof(x)))
            {
                return std::addressof(x);
            }
    
            return m_allocator.address(x);
        }
    
        template <class U>
        struct rebind { typedef stack_allocator<U, N, allocator_type> other; };
    
        pointer buffer() const noexcept
        {
            return m_begin;
        }
    
        private:
    
        bool pointer_to_internal_buffer(const_pointer p) const
        {
            return (!(std::less<const_pointer>()(p, m_begin)) && (std::less<const_pointer>()(p, m_end)));
        }
    
        allocator_type m_allocator;
        pointer m_begin;
        pointer m_end;
        pointer m_stack_pointer;
    };
    
    template <class T1, std::size_t N, class Allocator, class T2>
    bool operator == (const stack_allocator<T1, N, Allocator>& lhs, 
        const stack_allocator<T2, N, Allocator>& rhs) noexcept
    {
        return lhs.buffer() == rhs.buffer();
    }
    
    template <class T1, std::size_t N, class Allocator, class T2>
    bool operator != (const stack_allocator<T1, N, Allocator>& lhs, 
        const stack_allocator<T2, N, Allocator>& rhs) noexcept
    {
        return !(lhs == rhs);
    }
    


    此分配器使用用户提供的固定大小缓冲区作为初始内存源,然后在空间不足时回退到辅助分配器(默认为std::allocator&lt;T&gt;)。

    需要考虑的事项:

    在您继续使用堆栈分配器之前,您需要考虑您的分配模式。首先,在堆栈上使用内存缓冲区时,您需要考虑分配和释放内存的确切含义。

    最简单的方法(以及上面使用的方法)是简单地增加堆栈指针以进行分配,并减少堆栈指针以进行释放。请注意,这严重限制了您在实践中如何使用分配器。如果使用正确,它将适用于std::vector(它将分配一个连续的内存块),但不适用于std::map,它将以不同的顺序分配和取消分配节点对象。

    如果您的堆栈分配器只是增加和减少堆栈指针,那么如果您的分配和解除分配不是 LIFO 顺序,您将获得未定义的行为。即使是std::vector,如果它首先从堆栈中分配一个连续的块,然后分配第二个堆栈块,然后释放第一个块,就会导致未定义的行为,这将在每次向量将其容量增加到仍然存在的值时发生小于stack_size。这就是为什么您需要提前保留堆栈大小的原因。 (但请参阅下面有关 Howard Hinnant 实现的注释。)

    这给我们带来了问题......

    真正想要从堆栈分配器中得到什么?

    您是否真的想要一个通用分配器,它允许您以不同的顺序分配和释放各种大小的内存块(如malloc),除了它从预先分配的堆栈缓冲区中提取而不是调用@987654333 @?如果是这样,您基本上是在谈论实现一个通用分配器,它以某种方式维护内存块的空闲列表,只有用户可以为其提供预先存在的堆栈缓冲区。这是一个复杂得多的项目。 (如果空间用完了怎么办?扔std::bad_alloc?退回到堆上?)

    上面的实现假设您需要一个分配器,该分配器将简单地使用 LIFO 分配模式,如果空间不足,则回退到另一个分配器。这适用于std::vector,它将始终使用可以提前保留的单个连续缓冲区。当std::vector 需要更大的缓冲区时,它会分配更大的缓冲区,复制(或移动)较小缓冲区中的元素,然后释放较小的缓冲区。当向量请求更大的缓冲区时,上面的 stack_allocator 实现将简单地回退到辅助分配器(默认为std::allocator。)

    所以,例如:

    const static std::size_t stack_size = 4;
    int buffer[stack_size];
    
    typedef stack_allocator<int, stack_size> allocator_type;
    
    std::vector<int, allocator_type> vec((allocator_type(buffer))); // double parenthesis here for "most vexing parse" nonsense
    vec.reserve(stack_size); // attempt to reserve space for 4 elements
    
    std::cout << vec.capacity() << std::endl;
    
    vec.push_back(10);
    vec.push_back(20);
    vec.push_back(30);
    vec.push_back(40);
    
    // Assert that the vector is actually using our stack
    //
    assert(
        std::equal(
            vec.begin(), 
            vec.end(), 
            buffer, 
            [](const int& v1, const int& v2) {
                return &v1 == &v2;
            }
        )
    );
    
    // Output some values in the stack, we see it is the same values we
    // inserted in our vector.
    //
    std::cout << buffer[0] << std::endl;
    std::cout << buffer[1] << std::endl;
    std::cout << buffer[2] << std::endl;
    std::cout << buffer[3] << std::endl;
    
    // Attempt to push back some more values.  Since our stack allocator only has 
    // room for 4 elements, we cannot satisfy the request for an 8 element buffer.  
    // So, the allocator quietly falls back on using std::allocator.
    //
    // Alternatively, you could modify the stack_allocator implementation
    // to throw std::bad_alloc
    //
    vec.push_back(50);
    vec.push_back(60);
    vec.push_back(70);
    vec.push_back(80);
    
    // Assert that we are no longer using the stack buffer
    //
    assert(
        !std::equal(
            vec.begin(), 
            vec.end(), 
            buffer, 
            [](const int& v1, const int& v2) {
                return &v1 == &v2;
            }
        )
    );
    
    // Print out all the values in our vector just to make sure 
    // everything is sane.
    //
    for (auto v : vec) std::cout << v << ", ";
    std::cout << std::endl;
    

    见:http://ideone.com/YhMZxt

    同样,这对向量很有效——但你需要问问自己你打算用堆栈分配器做什么。如果您想要一个恰好从堆栈缓冲区中提取的通用内存分配器,那么您正在谈论一个更复杂的项目。然而,一个简单的堆栈分配器,它只增加和减少一个堆栈指针,将适用于有限的用例集。请注意,对于非 POD 类型,您需要使用 std::aligned_storage&lt;T, alignof(T)&gt; 来创建实际的堆栈缓冲区。

    我还要注意,与Howard Hinnant's implementation 不同,上述实现没有明确检查当您调用deallocate() 时,传入的指针是最后分配的块。如果传入的指针不是 LIFO 排序的释放,Hinnant 的实现将什么也不做。这将使您能够使用std::vector 而无需提前保留,因为分配器基本上会忽略向量释放初始缓冲区的尝试。但这也稍微模糊了分配器的语义,并且依赖于与std::vector 已知工作方式非常具体绑定的行为。我的感觉是,我们不妨简单地说,通过 last callallocate() 返回的任何指向deallocate() 的指针没有 返回将导致未定义的行为就这样吧。

    *最后 - 以下警告:检查指针是否在堆栈缓冲区边界内的函数似乎是 debatable 甚至是标准定义的行为。对来自不同new/malloc'd 缓冲区的两个指针进行顺序比较可以说是实现定义的行为(即使使用std::less),这可能使得无法编写符合标准的堆栈分配器实现依赖于堆分配. (但实际上这无关紧要,除非您在 MS-DOS 上运行 80286。)

    ** 最后(真的是现在),还值得注意的是,stack allocator 中的“stack”一词有点重载,指代内存的source (一个固定大小的堆栈数组)和分配的方法(一个LIFO递增/递减堆栈指针)。当大多数程序员说他们想要一个堆栈分配器时,他们考虑的是前者的含义,而不必考虑后者的语义,以及这些语义如何限制这种分配器与标准容器的使用。

    【讨论】:

    • 好答案。写。指针比较的问题,我曾经也考虑过这个问题 - 请参阅 stackoverflow.com/questions/8225501/… 了解与您链接的问题相关的问题。
    • void deallocate(pointer p, size_type n) 中有错误。我不认为你可以像这样减少 stack_pointer。
    【解决方案4】:

    这实际上是一种非常有用的做法,并且在性能开发(例如游戏)中使用得相当多。将内存内联嵌入堆栈或类结构的分配对于容器的速度和/或管理至关重要。

    要回答您的问题,它归结为 stl 容器的实现。如果容器不仅实例化而且还保持对分配器的引用作为成员,那么您最好创建一个固定堆,我发现情况并非总是如此,因为它不是规范的一部分。否则就会有问题。一种解决方案可以是用另一个包含存储的类来包装容器、向量、列表等。然后你可以使用分配器从中提取。这可能需要大量的模板魔法(tm)。

    【讨论】:

    • " 一种非常有用的做法,并在性能开发中使用,例如游戏,相当多" -- 需要引用 :-) ...您的第二段也是有点不清楚。您所说的“不仅是 inst。而且还保持引用......”是什么意思?
    • 我是一名游戏开发者,这个人说的太对了!堆栈分配器和容器结合使用的情况不胜枚举..
    • 除了它每个具有动态内存的stl容器都保留一个分配器副本的规范的一部分。
    【解决方案5】:

    基于堆栈的 STL 分配器的实用性非常有限,我怀疑您会发现很多现有技术。如果您后来决定要复制或延长最初的 lstring,即使您引用的简单示例也会很快爆炸。

    对于其他 STL 容器,例如关联容器(内部基于树)甚至 vectordeque,它们使用单​​个或多个连续 RAM 块,内存使用语义在堆栈上很快变得难以管理几乎所有真实世界的用法。

    【讨论】:

      【解决方案6】:

      这真的取决于你的要求,如果你愿意,你可以创建一个只在堆栈上运行的分配器,但它会非常有限,因为同一个堆栈对象不能从程序中的任何地方访问,因为堆对象将是.

      我认为这篇文章很好地解释了分配器

      http://www.codeguru.com/cpp/cpp/cpp_mfc/stl/article.php/c4079

      【讨论】:

      • 如果你愿意,你可以创建一个只在堆栈上运行的分配器 - 任何现有技术?我讨厌重新发明轮子:-)
      猜你喜欢
      • 2015-12-31
      • 2021-12-29
      • 1970-01-01
      • 1970-01-01
      • 2014-09-07
      • 1970-01-01
      • 1970-01-01
      • 2017-02-23
      相关资源
      最近更新 更多