Created
October 26, 2021 17:35
-
-
Save kdchambers/5d679c9c141d8a35a9ef6450327fbb50 to your computer and use it in GitHub Desktop.
Data structure that chains fixed sized arrays together to avoid unnecessary calls to heap
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// SPDX-License-Identifier: GPL-3.0 | |
// Copyright (c) 2021 Keith Chambers | |
// 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, version 3. | |
// NOTE: This snippet is taken from https://github.com/kdchambers/kpl/blob/master/include/kpl/chainedarray.h which | |
// is part of the kpl library. This data structure is used when you extent to use a reasonably small amount of data | |
// but need the assurance of being able to allocate large amounts of data in some rare scenarios. | |
#ifndef CHAINEDARRAY_H | |
#define CHAINEDARRAY_H | |
// #define CHAINED_ARRAY_DEBUG | |
#ifdef CHAINED_ARRAY_DEBUG | |
#include <fmt/format.h> | |
#include <assert.h> | |
#include <cstdio> | |
#include <functional> | |
#endif | |
#include <cstdlib> | |
#include <cstdint> | |
#include <cstring> | |
#include <kpl/array.h> | |
#include <kpl/util.h> | |
#include <kpl/pair.h> | |
/* Use case: When you are reasonably sure that you want to store a small amount of elements | |
* but need to ability to store far more to be safe */ | |
namespace kpl{ | |
template <typename T, std::size_t STACK_PRE_ALLOC, std::size_t DYNA_BUFFER_SIZE, std::size_t NUM_DYNA_BUFFERS> | |
class ChainedArray : public kpl::array<T> | |
{ | |
private: | |
uint16_t mNumElements; | |
uint8_t mDynaAllocatedMemoryBlocks; | |
uint8_t * mDynaMemBuffer[NUM_DYNA_BUFFERS]; | |
uint8_t mStackBuffer[sizeof(T) * STACK_PRE_ALLOC]; | |
const uint32_t MAX_CAPACITY = STACK_PRE_ALLOC + (DYNA_BUFFER_SIZE * NUM_DYNA_BUFFERS); | |
public: | |
ChainedArray() | |
: mNumElements { 0 }, | |
mDynaAllocatedMemoryBlocks {0} | |
{ | |
for(uint8_t i = 0; i < NUM_DYNA_BUFFERS; i++) | |
mDynaMemBuffer[i] = nullptr; | |
} | |
/* Getters */ | |
T t(uint16_t index) const override | |
{ | |
uint8_t * location = const_cast<uint8_t*>(addrForElement(index)); | |
return *(reinterpret_cast<T*>(location)); | |
} | |
T& tRef(uint16_t index) override | |
{ | |
uint8_t * location = const_cast<uint8_t*>(addrForElement(index)); | |
return *(reinterpret_cast<T*>(location)); | |
} | |
const T& cTRef(uint16_t index) const override | |
{ | |
const uint8_t * location = addrForElement(index); | |
return *(reinterpret_cast<const T*>(location)); | |
} | |
uint16_t size() const | |
{ | |
return mNumElements; | |
} | |
/* Modifiers */ | |
bool removeEnd() override | |
{ | |
if(mNumElements <= 0) | |
return false; | |
mNumElements--; | |
return true; | |
} | |
bool remove(uint16_t index) override | |
{ | |
if(index >= mNumElements) | |
return false; | |
// Left shift will overwrite index | |
leftShiftData(index, 1); | |
mNumElements--; | |
return true; | |
} | |
bool remove(uint16_t index, uint16_t numElements) override | |
{ | |
if(index + numElements >= mNumElements) | |
return false; | |
leftShiftData(index, numElements); | |
mNumElements -= numElements; | |
return true; | |
} | |
bool pushBack(const T& t) override | |
{ | |
if(mNumElements == MAX_CAPACITY) | |
{ | |
#ifdef CHAINED_ARRAY_DEBUG | |
assert(false && "Err: Insertion at max value"); | |
#endif | |
return false; | |
} | |
uint8_t * placement = prepareBackForPlacement(); | |
if(! placement) | |
{ | |
#ifdef CHAINED_ARRAY_DEBUG | |
assert(false && "Failed to push value element"); | |
#endif | |
return false; | |
} | |
new ( reinterpret_cast<void*>(placement) ) T( t ); | |
mNumElements++; | |
#ifdef CHAINED_ARRAY_DEBUG | |
fmt::print("Element added. Size -> {}\n", mNumElements); | |
#endif | |
return true; | |
} | |
bool pushBack(T&& t) override | |
{ | |
if(mNumElements == MAX_CAPACITY) | |
{ | |
#ifdef CHAINED_ARRAY_DEBUG | |
assert(false && "Err: Insertion at max value"); | |
#endif | |
return false; | |
} | |
uint8_t * placement = prepareBackForPlacement(); | |
if(! placement) | |
{ | |
#ifdef CHAINED_ARRAY_DEBUG | |
assert(false && "Failed to push r-value element"); | |
#endif | |
return false; | |
} | |
new ( reinterpret_cast<void*>(placement) ) T( std::move(t) ); | |
mNumElements++; | |
#ifdef CHAINED_ARRAY_DEBUG | |
fmt::print("Element added. Size -> {}\n", mNumElements); | |
#endif | |
return true; | |
} | |
bool insertAt(const T& t, uint16_t index) override | |
{ | |
if(mNumElements == MAX_CAPACITY) | |
{ | |
#ifdef CHAINED_ARRAY_DEBUG | |
assert(false && "Err: Insertion at max value"); | |
#endif | |
return false; | |
} | |
uint8_t * placement = prepareIndexForPlacement(index); | |
if(! placement) | |
{ | |
#ifdef CHAINED_ARRAY_DEBUG | |
assert(false && "Failed to insert value element by value"); | |
#endif | |
return false; | |
} | |
// Place new element in the cleared space | |
new ( placement ) T( t ); | |
mNumElements++; | |
return true; | |
} | |
bool insertAt(T&& t, uint16_t index) override | |
{ | |
if(mNumElements == MAX_CAPACITY) | |
{ | |
#ifdef CHAINED_ARRAY_DEBUG | |
assert(false && "Err: Insertion at max value"); | |
#endif | |
return false; | |
} | |
uint8_t * placement = prepareIndexForPlacement(index); | |
if(! placement) | |
{ | |
#ifdef CHAINED_ARRAY_DEBUG | |
fmt::print("Error: Failed to get placement address for insertAt op\n"); | |
dumpState(); | |
assert(false); | |
#endif | |
return false; | |
} | |
// Place new element in the cleared space | |
new ( placement ) T( std::move(t) ); | |
mNumElements++; | |
return true; | |
} | |
T * setNext() | |
{ | |
T * result = reinterpret_cast<T *>( prepareBackForPlacement() ); | |
mNumElements++; | |
return result; | |
} | |
/* Contructs a type T at the memory address calculate to placement using T's constructor values | |
* E.G Instead of push_back( t ), you do emplace_back( a, b, c ) where type T has a constructor of T(a, b, c */ | |
template <typename... Ts> | |
bool emplaceBack(Ts&&... args) | |
{ | |
if(mNumElements == MAX_CAPACITY) | |
{ | |
#ifdef CHAINED_ARRAY_DEBUG | |
assert(false && "Err: Insertion at max value"); | |
#endif | |
return false; | |
} | |
uint8_t * placement = (mNumElements < STACK_PRE_ALLOC) ? (&mStackBuffer[mNumElements]) : prepareBackForPlacement(); | |
if(! placement) | |
{ | |
#ifdef CHAINED_ARRAY_DEBUG | |
fmt::print("Error: Failed to get placement address for emplaceBack op\n"); | |
dumpState(); | |
assert(false); | |
#endif | |
return false; | |
} | |
new ( reinterpret_cast<void*>(placement) ) T( static_cast<Ts&&>(args)... ); | |
mNumElements++; | |
#ifdef CHAINED_ARRAY_DEBUG | |
fmt::print("Element added. Size -> {}\n", mNumElements); | |
#endif | |
return true; | |
} | |
#ifdef CHAINED_ARRAY_DEBUG | |
void printContents( std::function<void(const T)> printFunc) | |
{ | |
for(uint16_t i = 0; i < mNumElements; i++) | |
printFunc( cTRef(i) ); | |
} | |
#endif | |
template <typename... Ts> | |
bool emplaceAt(Ts&&... args, uint16_t index) | |
{ | |
if(mNumElements == MAX_CAPACITY) | |
{ | |
#ifdef CHAINED_ARRAY_DEBUG | |
assert(false && "Err: Insertion at max value"); | |
#endif | |
return false; | |
} | |
uint8_t * placement = prepareIndexForPlacement(index); | |
if(! placement) | |
{ | |
#ifdef CHAINED_ARRAY_DEBUG | |
assert(false && "Failed to push value element by value"); | |
#endif | |
return false; | |
} | |
new ( reinterpret_cast<void*>(placement) ) T( static_cast<Ts&&>(args)... ); | |
mNumElements++; | |
return true; | |
} | |
kpl::array<T> * asArray() | |
{ | |
return dynamic_cast<kpl::array<T> *>(this); | |
} | |
private: | |
// Assume valid dynaBufferIndex | |
bool ensureBufferAllocated(uint16_t dynaBufferIndex) | |
{ | |
if(dynaBufferIndex == 0) | |
return true; | |
if(mDynaMemBuffer[dynaBufferIndex - 1] == nullptr) | |
mDynaMemBuffer[dynaBufferIndex - 1] = reinterpret_cast<uint8_t*>( malloc( sizeof(T) * DYNA_BUFFER_SIZE ) ); | |
#ifdef CHAINED_ARRAY_DEBUG | |
fmt::print(stderr, "Dynamic Buffer # {} allocated\n", dynaBufferIndex - 1); | |
#endif | |
assert( mDynaMemBuffer[dynaBufferIndex - 1] ); | |
return true; | |
} | |
uint16_t bufferIndexFor(uint16_t index) const | |
{ | |
if(index < STACK_PRE_ALLOC) | |
return index; | |
uint16_t result = 1; | |
index -= STACK_PRE_ALLOC; | |
while(index >= DYNA_BUFFER_SIZE) | |
{ | |
result++; | |
index -= DYNA_BUFFER_SIZE; | |
} | |
return result; | |
} | |
const uint8_t * addrForElement(uint16_t index) const | |
{ | |
// Make sure requested index is in range | |
if( STACK_PRE_ALLOC + (DYNA_BUFFER_SIZE * NUM_DYNA_BUFFERS) <= index ) | |
{ | |
#ifdef CHAINED_ARRAY_DEBUG | |
fmt::print(stderr, "Error: Requested address exceeds max allocated memory in ChainedArray\n" | |
"Requested Index: {}\n" | |
"Preallocated Stack Memory: {}\n" | |
"Dynamic Memory Block Size: {}\n" | |
"Max Number Dyanamic Memory Blocks: {}\n", index, STACK_PRE_ALLOC, DYNA_BUFFER_SIZE, NUM_DYNA_BUFFERS); | |
exit(EXIT_FAILURE); | |
#endif | |
return nullptr; | |
} | |
kpl::Pair<uint16_t, uint16_t> absoluteIndexPair = translateIndex(index); | |
#ifdef CHAINED_ARRAY_DEBUG | |
fmt::print("Absolute Index Pair for Raw Index of {} -> {}:{}\n", index, absoluteIndexPair.getT1CRef(), absoluteIndexPair.getT2CRef()); | |
#endif | |
// Get address from stack memory | |
if(absoluteIndexPair.getT1CRef() == 0) | |
return reinterpret_cast<const uint8_t *>(& (mStackBuffer[index * sizeof(T)])); | |
// Since index has been localised to a memory block, it should not exceed the size of one | |
if(absoluteIndexPair.getT2CRef() > DYNA_BUFFER_SIZE) | |
{ | |
#ifdef CHAINED_ARRAY_DEBUG | |
fmt::print(stderr, "Error: index({}) > DYNA_BUFFER_SIZE({}) assert failed in ChainedArray::addrForElement({})\n", absoluteIndexPair.getT2CRef(), DYNA_BUFFER_SIZE, absoluteIndexPair.getT2CRef()); | |
assert(absoluteIndexPair.getT2CRef() <= DYNA_BUFFER_SIZE); | |
#endif | |
return nullptr; | |
} | |
// Get beginning of correct memory block and move index * sizeof(T) bytes forward | |
return reinterpret_cast<const uint8_t *>(mDynaMemBuffer[absoluteIndexPair.getT1CRef() - 1] + (absoluteIndexPair.getT2CRef() * sizeof(T))); | |
} | |
/* BufferIndex, RelativeIndex */ | |
kpl::Pair<uint16_t, uint16_t> translateIndex(uint16_t index) const | |
{ | |
if(index < STACK_PRE_ALLOC) | |
return kpl::Pair<uint16_t, uint16_t>(0, index); | |
uint16_t result = 1; | |
index -= STACK_PRE_ALLOC; | |
while(index >= DYNA_BUFFER_SIZE) | |
{ | |
result++; | |
index -= DYNA_BUFFER_SIZE; | |
} | |
return kpl::Pair<uint16_t, uint16_t>(result, index); | |
} | |
T* dataPtrAt(kpl::Pair<uint16_t, uint16_t> indexPair) | |
{ | |
if(indexPair.getT1CRef() == 0) | |
{ | |
return reinterpret_cast<T*>(& mStackBuffer[indexPair.getT2CRef() * sizeof(T)]); | |
} else | |
{ | |
return reinterpret_cast<T*>(mDynaMemBuffer[indexPair.getT1CRef() - 1] + (indexPair.getT2CRef() * sizeof(T))); | |
} | |
} | |
#ifdef CHAINED_ARRAY_DEBUG | |
void dumpState() | |
{ | |
fmt::print("Dumping chained buffer state\n\n"); | |
fmt::print("Stack allocated space: {}\n", STACK_PRE_ALLOC); | |
fmt::print("Dynamic buffer size: {}\n", DYNA_BUFFER_SIZE); | |
fmt::print("Number of Dynamic Buffers: {}\n", NUM_DYNA_BUFFERS); | |
fmt::print("Size: {}\n", mNumElements); | |
fmt::print("Tail buffer index: {}\n\n", bufferIndexFor(mNumElements)); | |
} | |
#endif | |
/* Shifts from the end until indexTo, indexTo is overridden */ | |
void leftShiftData(uint16_t indexTo, uint16_t shiftSize) | |
{ | |
if(indexTo >= mNumElements) | |
{ | |
#ifdef CHAINED_ARRAY_DEBUG | |
fmt::print("Err: indexTo in leftShiftData is greater than total number of elements in container\n"); | |
fmt::print("IndexTo: {}, mNumberElements: {}\n", indexTo, mNumElements); | |
assert(false); | |
#endif | |
return; | |
} | |
uint16_t currentIndex = indexTo + shiftSize; | |
uint16_t shiftToIndex = indexTo; | |
while(currentIndex < mNumElements) | |
{ | |
kpl::Pair<uint16_t, uint16_t> srcIndexPair = translateIndex(currentIndex); | |
kpl::Pair<uint16_t, uint16_t> dstIndexPair = translateIndex(shiftToIndex); | |
// *dataPtrAt(dstIndexPair) = *dataPtrAt(srcIndexPair); | |
std::memcpy(dataPtrAt(dstIndexPair), dataPtrAt(srcIndexPair), sizeof(T)); | |
shiftToIndex++; | |
currentIndex++; | |
} | |
} | |
/* TODO: Make more efficient */ | |
void rightShiftData(uint16_t indexFrom, uint16_t shiftSize) | |
{ | |
// Start at end of array and work towards in shift point | |
uint16_t shiftPointIndex = mNumElements - 1; | |
#ifdef CHAINED_ARRAY_DEBUG | |
kpl::Pair<uint16_t, uint16_t> testIndexPair = translateIndex(shiftPointIndex); | |
fmt::print("Shift point: raw -> {}, bufferIndex -> {}, localIndex -> {}\n", shiftPointIndex, testIndexPair.getT1CRef(), testIndexPair.getT2CRef()); | |
#endif | |
while( true ) | |
{ | |
kpl::Pair<uint16_t, uint16_t> srcIndexPair = translateIndex(shiftPointIndex); | |
kpl::Pair<uint16_t, uint16_t> dstIndexPair = translateIndex(shiftPointIndex + shiftSize); | |
#ifdef CHAINED_ARRAY_DEBUG | |
fmt::print("Moving data from index {}:{} to {}:{}\n", srcIndexPair.getT1CRef(), srcIndexPair.getT2CRef(), dstIndexPair.getT1CRef(), dstIndexPair.getT2CRef()); | |
#endif | |
std::memcpy(dataPtrAt(dstIndexPair), dataPtrAt(srcIndexPair), sizeof(T)); | |
if(shiftPointIndex-- == indexFrom) | |
break; | |
} | |
#ifdef CHAINED_ARRAY_DEBUG | |
fmt::print("Data shift completed\n"); | |
#endif | |
} | |
uint8_t * prepareBackForPlacement() | |
{ | |
// Fast path, stack only | |
if(mNumElements < STACK_PRE_ALLOC) | |
return &mStackBuffer[ mNumElements * sizeof(T) ]; | |
/* Slow path that needs to take dynamic buffers into account */ | |
// T1: bufferIndex, T2: Relative index | |
kpl::Pair<uint16_t, uint16_t> fullIndex = translateIndex(mNumElements); | |
if(! ensureBufferAllocated(fullIndex.getT1Ref())) | |
{ | |
#ifdef CHAINED_ARRAY_DEBUG | |
fmt::print("Error: Failed to allocate DYNAMIC buffer # {} for pushBack op\n", fullIndex.getT1CRef() - 1); | |
dumpState(); | |
assert(false); | |
#endif | |
return nullptr; | |
} | |
return mDynaMemBuffer[fullIndex.getT1CRef() - 1] + ( sizeof(T) * fullIndex.getT2CRef() ); | |
} | |
uint8_t * prepareIndexForPlacement(uint16_t index, uint16_t shiftSize = 1) | |
{ | |
if(! ensureBufferAllocated( bufferIndexFor( mNumElements + shiftSize - 1 ) )) | |
{ | |
#ifdef CHAINED_ARRAY_DEBUG | |
fmt::print("Error: Failed to allocate buffer index # {}\n", bufferIndexFor( mNumElements + shiftSize - 1 )); | |
dumpState(); | |
assert(false); | |
#endif | |
return nullptr; | |
} | |
// Make space for new element | |
rightShiftData(index, shiftSize); | |
return const_cast<uint8_t*>(addrForElement(index)); | |
} | |
}; | |
} | |
#endif // CHAINEDARRAY_H |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment