Head/Tail adaptive memory stack push

This commit is contained in:
Doyle 2018-09-03 00:02:32 +10:00
parent 43bc710dbd
commit fd228d092b

145
dqn.h
View File

@ -62,11 +62,6 @@
// - #DqnWin32 Common Win32 API Helpers // - #DqnWin32 Common Win32 API Helpers
// TODO // TODO
// - DqnMemStack
// - Allow 0 size memblock stack initialisation/block-less stack for situations where you don't
// care about specifying a size upfront
// - Default block size for when new blocks are required
//
// - Win32 // - Win32
// - Get rid of reliance on MAX_PATH // - Get rid of reliance on MAX_PATH
// //
@ -1287,9 +1282,9 @@ struct DqnArray
void Resize (isize new_len) { if (new_len > max) Reserve(GrowCapacity_(new_len)); len = new_len; } void Resize (isize new_len) { if (new_len > max) Reserve(GrowCapacity_(new_len)); len = new_len; }
void Resize (isize new_len, T const *v) { if (new_len > max) Reserve(GrowCapacity_(new_len)); if (new_len > len) for (isize n = len; n < new_len; n++) data[n] = *v; len = new_len; } void Resize (isize new_len, T const *v) { if (new_len > max) Reserve(GrowCapacity_(new_len)); if (new_len > len) for (isize n = len; n < new_len; n++) data[n] = *v; len = new_len; }
void Reserve (isize new_max); void Reserve (isize new_max);
T *Make (isize len = 1) { len += len; if (len > max) Reserve(GrowCapacity_(len)); return &data[len - len]; } T *Make (isize len_ = 1) { len += len_; if (len > max) Reserve(GrowCapacity_(len)); return &data[len - len_]; }
T *Push (T const &v) { if (len + 1 > max) Reserve(GrowCapacity_(len + 1)); data[len++] = v; return data + (len-1); } T *Push (T const &v) { if (len + 1 > max) Reserve(GrowCapacity_(len + 1)); data[len++] = v; return data + (len-1); }
T *Push (T const *v, isize v_len = 1) { isize new_len = len + v_len; if (new_len > max) Reserve(GrowCapacity_(new_len)); T *result = data + len; for (isize i = 0; i < new_len; ++i) data[len++] = v[i]; return result; } T *Push (T const *v, isize v_len) { isize new_len = len + v_len; if (new_len > max) Reserve(GrowCapacity_(new_len)); T *result = data + len; for (isize i = 0; i < v_len; ++i) data[len++] = v[i]; return result; }
void Pop () { if (len > 0) len--; } void Pop () { if (len > 0) len--; }
void Erase (isize index) { DQN_ASSERT(index >= 0 && index < len); data[index] = data[--len]; } void Erase (isize index) { DQN_ASSERT(index >= 0 && index < len); data[index] = data[--len]; }
void EraseStable(isize index); void EraseStable(isize index);
@ -1404,18 +1399,26 @@ struct DqnMemTracker
// #DqnMemStack API // #DqnMemStack API
// ================================================================================================= // =================================================================================================
// DqnMemStack is an memory allocator in a stack like, push-pop style. It pre-allocates a block of // DqnMemStack is a memory allocator in a stack like, push-pop style. It
// memory at init and sub-allocates from this block to take advantage of memory locality. // pre-allocates a block of memory at init and sub-allocates from this block to
// take advantage of memory locality. You can allocate and grow the stack from
// the bottom and/or allocate from the top and grow downwards until space is
// insufficient in the memory block.
// When an allocation requires a larger amount of memory than available in the block then the // When an allocation requires a larger amount of memory than available in the
// MemStack will allocate a new block of sufficient size for you in DqnMemStack_Push(..). This DOES // block then the MemStack will allocate a new block of sufficient size for you
// mean that there will be wasted spaceat the end of each block and is a tradeoff for memory // in Push(..). This DOES mean that occasionally there will be wasted space at
// locality against optimal space usage. // the end of each block and is a tradeoff for memory locality against optimal
// space usage.
// How To Use: // My convention with using a memory stack is, any function that takes a memory
// DqnMemStack stack = {}; // stack must revert all allocations from the the end of the stack in the scope
// stack.Init(DQN_MEGABYTE(1), true, DqnMemStack::Flag::BoundsGuard); // it was used in. If the function takes an allocator, then it must always do
// u8 *data = stack.Push(128); // some operation that allocates persistent data into the head of the allocator.
// If a function requires an allocator purely for temporary purposes, then there
// always exists some non-user facing global allocator that is accessible which
// it can use.
#define DQN_MEMSTACK_PUSH_STRUCT(memstack, Type) (Type *)(memstack)->Push(sizeof(Type)) #define DQN_MEMSTACK_PUSH_STRUCT(memstack, Type) (Type *)(memstack)->Push(sizeof(Type))
#define DQN_MEMSTACK_PUSH_ARRAY(memstack, Type, num) (Type *)(memstack)->Push(sizeof(Type) * (num)) #define DQN_MEMSTACK_PUSH_ARRAY(memstack, Type, num) (Type *)(memstack)->Push(sizeof(Type) * (num))
@ -1454,31 +1457,63 @@ struct DqnMemStack
isize Usage() const { return size - (tail - head); } isize Usage() const { return size - (tail - head); }
}; };
DqnMemTracker tracker; // Read: Metadata for managing ptr allocation DqnMemTracker tracker; // Metadata for managing ptr allocation
DqnAllocator *block_allocator; // Read: Allocator used to allocator blocks for this memory stack DqnAllocator *block_allocator; // Allocator used to allocator blocks for this memory stack
Block *block; // Read: Memory block allocated for the stack Block *block; // Memory block allocated for the stack
u32 flags; // Read u32 flags;
i32 mem_region_count;// Read: The number of temp memory regions in use i32 mem_region_count; // The number of temp memory regions in use
DqnMemStack() = default; // Initialisation API
// =============================================================================================
DqnMemStack() = default; // Zero Is Initialisation, on first allocation LazyInit() is called.
DqnMemStack(void *mem, isize size, Dqn::ZeroMem clear, u32 flags_ = 0); // Use fixed memory from the given buffer. Assert after buffer is full.
// Uses fixed memory, will be sourced from the buffer and assert after buffer is full. // Init and alloc additional memory blocks when full, but only if NonExpandable flag is not set.
DqnMemStack(void *mem, isize size, Dqn::ZeroMem clear, u32 flags_ = 0);
// Memory Stack capable of expanding when full, but only if NonExpandable flag is not set.
DqnMemStack (isize size, Dqn::ZeroMem clear, u32 flags_ = 0, DqnAllocator *block_allocator_ = DQN_DEFAULT_ALLOCATOR) { LazyInit(size, clear, flags_, block_allocator_); } DqnMemStack (isize size, Dqn::ZeroMem clear, u32 flags_ = 0, DqnAllocator *block_allocator_ = DQN_DEFAULT_ALLOCATOR) { LazyInit(size, clear, flags_, block_allocator_); }
void LazyInit(isize size, Dqn::ZeroMem clear, u32 flags_ = 0, DqnAllocator *block_allocator_ = DQN_DEFAULT_ALLOCATOR); void LazyInit(isize size, Dqn::ZeroMem clear, u32 flags_ = 0, DqnAllocator *block_allocator_ = DQN_DEFAULT_ALLOCATOR);
// Allocation API // Allocation API
// ============================================================================================= // =============================================================================================
enum struct PushType { Default, Head, Tail }; enum struct AllocMode
{
Head, // Set the default to allocating to the start of the memory block
Tail, // Set the default to allocating from the end of the memory block
};
enum struct PushType
{
Default, // Allocation defaults to the AllocMode set in SetAllocMode(), by default this is initialised to be from the start of the memory stack.
Opposite, // Opposite is always the opposite side of the current default allocation mode.
// TODO(doyle): Head and Tail are possibly redundant modes now. When the
// client changes the default allocation mode outside of functions that
// take a memory stack as an allocator, it means if the function wants
// to use the "temporary" side of the memory stack, they need to be
// aware of what the opposite side of the memory stack is as to not
// trample over the memory that the client wants allocated memory from.
// This is what the 2 new PushType options above facilitate. I can't
// immediately see a situation now where you want to actually specify
// the side and override the allocation mode which could make for
// unintuitive behaviour, but I'm going to sit on this for awhile and
// see how the actual real world use cases pan out.
Head, // Ignores the AllocMode, always allocate from the start of the memory block
Tail, // Ignores the AllocMode, always allocate from the end of the memory block
};
// Allocate memory from the MemStack. // Allocate memory from the MemStack.
// alignment: Ptr returned from allocator is aligned to this value and MUST be power of 2. // alignment: Must be a power of 2, ptr returned from allocator is address aligned to this value.
// return: nullptr if out of space OR stack is using fixed memory/size and out of memory OR stack full and malloc fails (or flags prevent expansion). // return: Ptr to memory, nullptr if out of space and is disallowed to expand OR the stack is full and malloc fails
void *Push (isize size, PushType push_type = PushType::Default, u8 alignment = 4); void *Push (isize size, PushType push_type = PushType::Default, u8 alignment = 4);
void SetDefaultAllocate(PushType type) { if (type == PushType::Default) return; if (type == PushType::Head) flags &= ~Flag::DefaultAllocateTail; else flags |= Flag::DefaultAllocateTail; } // On Push, if push_type is PushType::Default, it will behave according to the AllocMode last set.
void SetAllocMode (AllocMode mode) { if (mode == AllocMode::Head) flags &= ~Flag::DefaultAllocateTail; else flags |= Flag::DefaultAllocateTail; }
// TODO(doyle): Edge case where Pop fails. If you Push to the head and it
// fits in the existing block. Then push to the tail a size larger than the
// remaining space and generate a new block, then, if you try to Pop the ptr
// from the head, it will not find the right block to pop from and assert.
void Pop (void *ptr, Dqn::ZeroMem zero = Dqn::ZeroMem::No); // Free the ptr. MUST be the last allocated ptr on the block head or tail, assert otherwise. void Pop (void *ptr, Dqn::ZeroMem zero = Dqn::ZeroMem::No); // Free the ptr. MUST be the last allocated ptr on the block head or tail, assert otherwise.
void PopBlock () { FreeBlock(block); } void PopBlock () { FreeBlock(block); }
void Free () { if (flags & Flag::BoundsGuard) tracker.allocations.Free(); while (block_allocator && block) PopBlock(); } void Free () { if (flags & Flag::BoundsGuard) tracker.allocations.Free(); while (block_allocator && block) PopBlock(); }
@ -2129,6 +2164,7 @@ struct DqnVHashTable
Bucket *buckets; Bucket *buckets;
isize num_buckets; isize num_buckets;
isize *indexes_of_used_buckets; isize *indexes_of_used_buckets;
isize num_used_entries;
isize num_used_buckets; isize num_used_buckets;
DqnVHashTable () = default; DqnVHashTable () = default;
@ -2215,9 +2251,9 @@ DQN_VHASH_TABLE_TEMPLATE typename DQN_VHASH_TABLE_DECL::Iterator &DQN_VHASH_TABL
DQN_VHASH_TABLE_TEMPLATE void DQN_VHASH_TABLE_DECL::LazyInit(isize size) DQN_VHASH_TABLE_TEMPLATE void DQN_VHASH_TABLE_DECL::LazyInit(isize size)
{ {
*this = {}; *this = {};
this->num_buckets = size; this->num_buckets = size;
this->buckets = static_cast<Bucket *>(DqnOS_VAlloc(size * sizeof(*this->buckets))); this->buckets = static_cast<Bucket *>(DqnOS_VAlloc(size * sizeof(*this->buckets)));
this->indexes_of_used_buckets = static_cast<isize *> (DqnOS_VAlloc(size * sizeof(*this->indexes_of_used_buckets))); this->indexes_of_used_buckets = static_cast<isize *> (DqnOS_VAlloc(size * sizeof(*this->indexes_of_used_buckets)));
DQN_ASSERT(this->buckets && this->indexes_of_used_buckets); DQN_ASSERT(this->buckets && this->indexes_of_used_buckets);
} }
@ -2268,6 +2304,7 @@ DQN_VHASH_TABLE_TEMPLATE Item *DQN_VHASH_TABLE_DECL::GetOrMake(Key const &key, b
entry = bucket->entries + bucket->entry_index++; entry = bucket->entries + bucket->entry_index++;
entry->key = key; entry->key = key;
++this->num_used_entries;
// TODO(doyle): A maybe case. We're using virtual memory, so you should // TODO(doyle): A maybe case. We're using virtual memory, so you should
// just initialise a larger size. It's essentially free ... maybe one // just initialise a larger size. It's essentially free ... maybe one
@ -2292,27 +2329,31 @@ DQN_VHASH_TABLE_TEMPLATE void DQN_VHASH_TABLE_DECL::Erase(Key const &key)
DQN_FOR_EACH(i, bucket->entry_index) DQN_FOR_EACH(i, bucket->entry_index)
{ {
Entry *check = bucket->entries + i; Entry *check = bucket->entries + i;
if (Equals(check->key, key)) if (!Equals(check->key, key))
{ {
for (isize j = i; j < (bucket->entry_index - 1); ++j) continue;
bucket->entries[j] = bucket->entries[j + 1]; }
if (--bucket->entry_index == 0) for (isize j = i; j < (bucket->entry_index - 1); ++j)
bucket->entries[j] = bucket->entries[j + 1];
--this->num_used_entries;
if (--bucket->entry_index == 0)
{
DQN_FOR_EACH(bucketIndex, this->num_used_buckets)
{ {
DQN_FOR_EACH(bucketIndex, this->num_used_buckets) if (this->indexes_of_used_buckets[bucketIndex] == index)
{ {
if (this->indexes_of_used_buckets[bucketIndex] == index) indexes_of_used_buckets[bucketIndex] =
{ indexes_of_used_buckets[--this->num_used_buckets];
indexes_of_used_buckets[bucketIndex] =
indexes_of_used_buckets[--this->num_used_buckets];
}
} }
} }
DQN_ASSERT(this->num_used_buckets >= 0);
DQN_ASSERT(bucket->entry_index >= 0);
return;
} }
DQN_ASSERT(this->num_used_entries >= 0);
DQN_ASSERT(this->num_used_buckets >= 0);
DQN_ASSERT(bucket->entry_index >= 0);
return;
} }
} }
@ -2978,10 +3019,10 @@ void *DqnMemStack::Push(isize size, PushType push_type, u8 alignment)
isize size_to_alloc = this->tracker.GetAllocationSize(size, alignment); isize size_to_alloc = this->tracker.GetAllocationSize(size, alignment);
bool push_to_head = true; bool push_to_head = true;
if (push_type == PushType::Default) if (push_type == PushType::Default || push_type == PushType::Opposite)
{ {
if (this->flags & Flag::DefaultAllocateTail) push_to_head = false; push_to_head = !(this->flags & Flag::DefaultAllocateTail);
else push_to_head = true; if (push_type == PushType::Opposite) push_to_head = !push_to_head;
} }
else else
{ {
@ -8293,7 +8334,7 @@ void *DqnOS_VAlloc(isize size, void *base_addr)
void *result = nullptr; void *result = nullptr;
#if defined (DQN_IS_WIN32) #if defined (DQN_IS_WIN32)
result = VirtualAlloc(base_addr, size, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE); result = VirtualAlloc(base_addr, size, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE);
DQN_ASSERT(result); DQN_ASSERTM(result, "VirtualAlloc failed: %s\n", DqnWin32_GetLastError());
#else #else
result = mmap( result = mmap(
base_addr, size, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1 /*fd*/, 0 /*offset into fd*/); base_addr, size, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1 /*fd*/, 0 /*offset into fd*/);