This repository has been archived on 2023-10-09. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
blender-archive/source/blender/blenlib/BLI_stack.hh
Jacques Lucke ccc2a7996b BLI: add typedefs for containers that use raw allocators
Those are useful when you have to create containers with static
storage duration. If those would use Blender's guarded allocator,
it would report memory leaks, that are not actually leaks.
2020-07-20 16:03:14 +02:00

391 lines
11 KiB
C++

/*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
#ifndef __BLI_STACK_HH__
#define __BLI_STACK_HH__
/** \file
* \ingroup bli
*
* A `blender::Stack<T>` is a dynamically growing FILO (first-in, last-out) data structure. It is
* designed to be a more convenient and efficient replacement for `std::stack`.
*
* The improved efficiency is mainly achieved by supporting small buffer optimization. As long as
* the number of elements added to the stack stays below InlineBufferCapacity, no heap allocation
* is done. Consequently, values stored in the stack have to be movable and they might be moved,
* when the stack is moved.
*
* A Vector can be used to emulate a stack. However, this stack implementation is more efficient
* when all you have to do is to push and pop elements. That is because a vector guarantees that
* all elements are in a contiguous array. Therefore, it has to copy all elements to a new buffer
* when it grows. This stack implementation does not have to copy all previously pushed elements
* when it grows.
*
* blender::Stack is implemented using a double linked list of chunks. Each chunk contains an array
* of elements. The chunk size increases exponentially with every new chunk that is required. The
* lowest chunk, i.e. the one that is used for the first few pushed elements, is embedded into the
* stack.
*/
#include "BLI_allocator.hh"
#include "BLI_memory_utils.hh"
#include "BLI_span.hh"
namespace blender {
/**
* A StackChunk references a contiguous memory buffer. Multiple StackChunk instances are linked in
* a double linked list.
*/
template<typename T> struct StackChunk {
/** The below chunk contains the elements that have been pushed on the stack before. */
StackChunk *below;
/** The above chunk contains the elements that have been pushed on the stack afterwards. */
StackChunk *above;
/** Pointer to the first element of the referenced buffer. */
T *begin;
/** Pointer to one element past the end of the referenced buffer. */
T *capacity_end;
int64_t capacity() const
{
return capacity_end - begin;
}
};
template<
/** Type of the elements that are stored in the stack. */
typename T,
/**
* The number of values that can be stored in this stack, without doing a heap allocation.
* Sometimes it can make sense to increase this value a lot. The memory in the inline buffer is
* not initialized when it is not needed.
*/
int64_t InlineBufferCapacity = default_inline_buffer_capacity(sizeof(T)),
/**
* The allocator used by this stack. Should rarely be changed, except when you don't want that
* MEM_* is used internally.
*/
typename Allocator = GuardedAllocator>
class Stack {
private:
using Chunk = StackChunk<T>;
/**
* Points to one element after top-most value in the stack.
*
* Invariant:
* If size_ == 0
* then: top_ == inline_chunk_.begin
* else: &peek() == top_ - 1;
*/
T *top_;
/** Points to the chunk that references the memory pointed to by top_. */
Chunk *top_chunk_;
/**
* Number of elements in the entire stack. The sum of initialized element counts in the chunks.
*/
int64_t size_;
/** The buffer used to implement small object optimization. */
TypedBuffer<T, InlineBufferCapacity> inline_buffer_;
/**
* A chunk referencing the inline buffer. This is always the bottom-most chunk.
* So inline_chunk_.below == nullptr.
*/
Chunk inline_chunk_;
/** Used for allocations when the inline buffer is not large enough. */
Allocator allocator_;
public:
/**
* Initialize an empty stack. No heap allocation is done.
*/
Stack(Allocator allocator = {}) : allocator_(allocator)
{
inline_chunk_.below = nullptr;
inline_chunk_.above = nullptr;
inline_chunk_.begin = inline_buffer_;
inline_chunk_.capacity_end = inline_buffer_ + InlineBufferCapacity;
top_ = inline_buffer_;
top_chunk_ = &inline_chunk_;
size_ = 0;
}
/**
* Create a new stack that contains the given elements. The values are pushed to the stack in
* the order they are in the array.
*/
Stack(Span<T> values) : Stack()
{
this->push_multiple(values);
}
/**
* Create a new stack that contains the given elements. The values are pushed to the stack in the
* order they are in the array.
*
* Example:
* Stack<int> stack = {4, 5, 6};
* assert(stack.pop() == 6);
* assert(stack.pop() == 5);
*/
Stack(const std::initializer_list<T> &values) : Stack(Span<T>(values))
{
}
Stack(const Stack &other) : Stack(other.allocator_)
{
for (const Chunk *chunk = &other.inline_chunk_; chunk; chunk = chunk->above) {
const T *begin = chunk->begin;
const T *end = (chunk == other.top_chunk_) ? other.top_ : chunk->capacity_end;
this->push_multiple(Span<T>(begin, end - begin));
}
}
Stack(Stack &&other) noexcept : Stack(other.allocator_)
{
uninitialized_relocate_n<T>(
other.inline_buffer_, std::min(other.size_, InlineBufferCapacity), inline_buffer_);
inline_chunk_.above = other.inline_chunk_.above;
size_ = other.size_;
if (inline_chunk_.above != nullptr) {
inline_chunk_.above->below = &inline_chunk_;
}
if (size_ <= InlineBufferCapacity) {
top_chunk_ = &inline_chunk_;
top_ = inline_buffer_ + size_;
}
else {
top_chunk_ = other.top_chunk_;
top_ = other.top_;
}
other.size_ = 0;
other.inline_chunk_.above = nullptr;
other.top_chunk_ = &other.inline_chunk_;
other.top_ = other.top_chunk_->begin;
}
~Stack()
{
this->destruct_all_elements();
Chunk *above_chunk;
for (Chunk *chunk = inline_chunk_.above; chunk; chunk = above_chunk) {
above_chunk = chunk->above;
allocator_.deallocate(chunk);
}
}
Stack &operator=(const Stack &stack)
{
if (this == &stack) {
return *this;
}
this->~Stack();
new (this) Stack(stack);
return *this;
}
Stack &operator=(Stack &&stack)
{
if (this == &stack) {
return *this;
}
this->~Stack();
new (this) Stack(std::move(stack));
return *this;
}
/**
* Add a new element to the top of the stack.
*/
void push(const T &value)
{
if (top_ == top_chunk_->capacity_end) {
this->activate_next_chunk(1);
}
new (top_) T(value);
top_++;
size_++;
}
void push(T &&value)
{
if (top_ == top_chunk_->capacity_end) {
this->activate_next_chunk(1);
}
new (top_) T(std::move(value));
top_++;
size_++;
}
/**
* Remove and return the top-most element from the stack. This invokes undefined behavior when
* the stack is empty.
*/
T pop()
{
BLI_assert(size_ > 0);
top_--;
T value = std::move(*top_);
top_->~T();
size_--;
if (top_ == top_chunk_->begin) {
if (top_chunk_->below != nullptr) {
top_chunk_ = top_chunk_->below;
top_ = top_chunk_->capacity_end;
}
}
return value;
}
/**
* Get a reference to the top-most element without removing it from the stack. This invokes
* undefined behavior when the stack is empty.
*/
T &peek()
{
BLI_assert(size_ > 0);
BLI_assert(top_ > top_chunk_->begin);
return *(top_ - 1);
}
const T &peek() const
{
BLI_assert(size_ > 0);
BLI_assert(top_ > top_chunk_->begin);
return *(top_ - 1);
}
/**
* Add multiple elements to the stack. The values are pushed in the order they are in the array.
* This method is more efficient than pushing multiple elements individually and might cause less
* heap allocations.
*/
void push_multiple(Span<T> values)
{
Span<T> remaining_values = values;
while (!remaining_values.is_empty()) {
if (top_ == top_chunk_->capacity_end) {
this->activate_next_chunk(remaining_values.size());
}
const int64_t remaining_capacity = top_chunk_->capacity_end - top_;
const int64_t amount = std::min(remaining_values.size(), remaining_capacity);
uninitialized_copy_n(remaining_values.data(), amount, top_);
top_ += amount;
remaining_values = remaining_values.drop_front(amount);
}
size_ += values.size();
}
/**
* Returns true when the size is zero.
*/
bool is_empty() const
{
return size_ == 0;
}
/**
* Returns the number of elements in the stack.
*/
int64_t size() const
{
return size_;
}
/**
* Removes all elements from the stack. The memory is not freed, so it is more efficient to reuse
* the stack than to create a new one.
*/
void clear()
{
this->destruct_all_elements();
top_chunk_ = &inline_chunk_;
top_ = top_chunk_->begin;
}
private:
/**
* Changes top_chunk_ to point to a new chunk that is above the current one. The new chunk might
* be smaller than the given size_hint. This happens when a chunk that has been allocated before
* is reused. The size of the new chunk will be at least one.
*
* This invokes undefined behavior when the currently active chunk is not full.
*/
void activate_next_chunk(const int64_t size_hint)
{
BLI_assert(top_ == top_chunk_->capacity_end);
if (top_chunk_->above == nullptr) {
const int64_t new_capacity = std::max(size_hint, top_chunk_->capacity() * 2 + 10);
/* Do a single memory allocation for the Chunk and the array it references. */
void *buffer = allocator_.allocate(
sizeof(Chunk) + sizeof(T) * new_capacity + alignof(T), alignof(Chunk), AT);
void *chunk_buffer = buffer;
void *data_buffer = (void *)(((uintptr_t)buffer + sizeof(Chunk) + alignof(T) - 1) &
~(alignof(T) - 1));
Chunk *new_chunk = new (chunk_buffer) Chunk();
new_chunk->begin = (T *)data_buffer;
new_chunk->capacity_end = new_chunk->begin + new_capacity;
new_chunk->above = nullptr;
new_chunk->below = top_chunk_;
top_chunk_->above = new_chunk;
}
top_chunk_ = top_chunk_->above;
top_ = top_chunk_->begin;
}
void destruct_all_elements()
{
for (T *value = top_chunk_->begin; value != top_; value++) {
value->~T();
}
for (Chunk *chunk = top_chunk_->below; chunk; chunk = chunk->below) {
for (T *value = chunk->begin; value != chunk->capacity_end; value++) {
value->~T();
}
}
}
};
/**
* Same as a normal Stack, but does not use Blender's guarded allocator. This is useful when
* allocating memory with static storage duration.
*/
template<typename T, int64_t InlineBufferCapacity = default_inline_buffer_capacity(sizeof(T))>
using RawStack = Stack<T, InlineBufferCapacity, RawAllocator>;
} /* namespace blender */
#endif /* __BLI_STACK_HH__ */