基于堆栈缓冲的STL分配器?

时间:2011-11-08 11:26:44

标签: c++ stl stack allocator

我想知道是否有一个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

6 个答案:

答案 0 :(得分:19)

肯定可以创建完全符合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<T>)。

需要考虑的事项:

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

最简单的方法(以及上面采用的方法)是简单地递增用于分配的堆栈指针,并将其递减以用于解除分配。请注意,严重限制了在实践中如何使用分配器。如果使用正确,它将适用于std::vector(它将分配一个连续的内存块),但是不会起作用std::map,它将分配和释放节点对象变化的顺序。

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

这让我们想到了这个问题......

你从堆栈分配器真正想要什么

你真的想要一个通用的分配器,它允许你以不同的顺序分配和释放各种大小的内存块(比如sbrk),除了它从预先分配的堆栈缓冲区中抽取而不是调用std::bad_alloc?如果是这样,你基本上是在谈论实现一个通用的分配器,它以某种方式维护一个空闲的内存块列表,只有用户可以为它提供一个预先存在的堆栈缓冲区。这是一个更复杂的项目。 (如果它耗尽空间应该怎么办?抛出std::vector?回到堆上?)

上面的实现假设您需要一个仅使用LIFO分配模式的分配器,如果空间不足,则需要使用另一个分配器。这适用于std::vector,它将始终使用可以提前保留的单个连续缓冲区。当std::allocator需要更大的缓冲区时,它将分配更大的缓冲区,复制(或移动)较小缓冲区中的元素,然后释放较小的缓冲区。当向量请求更大的缓冲区时,上面的stack_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; 。)

所以,例如:

std::aligned_storage<T, alignof(T)>

请参阅:http://ideone.com/YhMZxt

同样,这适用于矢量 - 但你需要问问自己你打算用堆栈分配器做什么。如果你想要一个恰好从堆栈缓冲区中抽取的通用内存分配器,你就会谈论一个更复杂的项目。然而,一个简单的堆栈分配器,它只是增加和减少堆栈指针,将适用于一组有限的用例。请注意,对于非POD类型,您需要使用deallocate()来创建实际的堆栈缓冲区。

我还注意到,与Howard Hinnant's implementation不同,上述实现并未明确检查当您调用std::vector时,传入的指针是分配的最后一个块。如果传入的指针不是LIFO排序的重新分配,Hinnant的实现将不会做任何事情。这将使您能够使用std::vector而无需提前预留,因为分配器基本上忽略向量尝试释放初始缓冲区。但这也模糊了分配器的语义,并且依赖于与deallocate()已知的工作方式非常具体相关的行为。我的感觉是,我们也可以简单地说,通过最后一次调用将{em> 返回的allocate()的任何指针传递给{{1}将导致未定义的行为并将其留在那。

*最后 - 以下警告:似乎是debatable是否检查指针是否在堆栈缓冲区边界内的函数甚至是标准定义的行为。顺序 - 比较来自不同new / malloc&#39; d缓冲区的两个指针可以说是实现定义的行为(即使使用std::less),这可能使得编写符合标准的行为成为不可能堆栈分配器实现,它依赖于堆分配。 (但在实践中,除非你在MS-DOS上运行80286,否则这无所谓。)

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

答案 1 :(得分:8)

显然,来自is a conforming Stack AllocatorHoward Hinnant

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

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

  

我已经使用符合C ++ 11标准的新分配器更新了本文。

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

答案 2 :(得分:2)

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

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

答案 3 :(得分:2)

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

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

答案 4 :(得分:1)

这实际上取决于您的要求,如果您愿意,可以确定您可以创建仅在堆栈上运行的分配器,但它将非常有限,因为无法从程序中的任何位置访问相同的堆栈对象,因为堆对象将是

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

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

答案 5 :(得分: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;                                                                                                                             
  }                                                                                                                                                          
};                                                                                                                                                           

这是实际的分配器。请注意,向资源管理器添加一个重置很简单,让您可以从该区域的开头重新创建一个新的分配器。也可以实现环形缓冲区,并具有所有通常的风险。

关于何时可能需要这样的东西:我在嵌入式系统中使用它。嵌入式系统通常对堆碎片反应不佳,因此有时可以使用不会在堆上进行的动态分配。