Memory Management
NoesisEngine is designed to allow controlling how memory allocations and deallocations are managed. Different layers are defined. At the lowest level we find the Kernel Memory Manager.
Kernel MemoryManager
The MemoryManager class is the Kernel module in charge of memory allocations during the Kernel lifetime. After initializing the Kernel, the memory manager is ready to be used by any package.
The memory manager offers two kind of functionality: memory tracking and memory allocation.
Memory Tracking
Memory manager holds information about every memory block that is allocated. This information is stored inside the struct MemBlockInfo.
struct MemBlockInfo
{
void* userPtr; // Pointer returned to the user
void* blockPtr; // Pointer of the whole memory block
MemBlockInfo* prev; // Link to the previus block in the allocation block list
MemBlockInfo* next; // Link to the next block in the allocation block list
NsInt64* callStack; // Pointer to the call stack of the allocation. Could be null
NsSize callStackDepth; // Call stack depth
NsInt32 allocId; // Identifier assigned to each allocation operation
NsUInt32 threadId; // Identifier of the thread that is performing the allocation operation
NsSize size; // Size requested by the user
};
This information is used to generate reports like, for example, memory leaks when the kernel is being closed. Memory-Leak reports can be accompanied with call-stack for each allocated block if those information is available. If memory tracker is enabled, when the memory manager is shut down the remaining allocated blocks are dumped to the console as memory leaks.
Also, you can get a snapshot of the current memory status, and use that information to calculate memory consumption of some code blocks. This is achieved with the GetStats() function.
struct MemoryManager::Stats
{
NsUInt32 allocs; // current allocated blocks
NsUInt32 allocsTotal; // total allocation operations
NsSize memory; // current memory used
NsSize memoryMax; // maximum memory used
NsSize memoryTotal; // total memory requested
NsSize memoryWasted; // current memory wasted by memory manager data
NsSize memoryWastedTotal; // total memory wasted by memory manager data
};
Memory Allocations
Memory allocations are not directly performed by the memory manager. This task is delegated to another class, the MemoryAllocator.
class MemoryAllocator
{
public:
/// Allocates a block of memory of specified size
/// \param size Size in bytes of the block requested by the user
/// \param alignment Pointer returned must be aligned to this value
/// \return A pointer to the new allocated block of memory
/// \remarks Thread-safe
virtual void* Alloc(NsSize size,
NsSize alignment = NS_DEFAULT_MEMORY_ALIGNMENT) = 0;
/// Reallocates a block of memory
/// \param ptr Pointer to previously allocated memory block
/// \param size New size in bytes
/// \param alignment Pointer returned must be aligned to this value
/// \return A pointer to the new allocated block of memory
/// \remarks Thread-safe
virtual void* Realloc(void* ptr,
NsSize size,
NsSize alignment = NS_DEFAULT_MEMORY_ALIGNMENT) = 0;
/// Deallocates a block of memory
/// \param ptr Pointer to previously allocated memory block
/// \remarks Thread-safe
virtual void Dealloc(void* ptr) = 0;
};
This layer is defined to allow developers to provide its own allocation algorithms. When Kernel is initialized, the user could supply a custom MemoryAllocator object. This object is passed to memory manager and is used to perform all allocation operations. NoesisEngine implements a default MemoryAllocator based on standard malloc() and free() functions, the AnsiAllocator. That one is used if no allocator is given.
The allocator to be used by the Kernel is passed to the Init() function.
class Kernel
{
...
/// Initializes the kernel
virtual void Init(const CommandLine& commandLine, MemoryAllocator* memoryAllocator = 0) = 0;
...
};
Configuration
The Configuration Mechanism is used to activate the different debug features of the Memory Manager. The following options are available:
- Kernel.MemoryManager.TrackMemory. When this option is true, memory manager tracking is enabled. The default value is false.
- Kernel.MemoryManager.StoreCallStack. This option controls if a call stack is generated for each allocation. It is only available if tracking is enabled. The default value is false.
- Kernel.MemoryManager.CallStackDepth. This options sets the depth of the call stack. It is only available if call stacks are stored. The default value is 4.
Global operators
NsAlloc, NsRealloc, NsDealloc
NsAlloc, NsRealloc and NsDealloc are shortcuts to the functions that are available in the memory manager. These functions can't be used if kernel is not initialized.
////////////////////////////////////////////////////////////////////////////////////////////////////
/// NoesisEngine global memory management.
////////////////////////////////////////////////////////////////////////////////////////////////////
//@{
NS_CORE_KERNEL_API
void* NsAlloc(NsSize size, NsUInt alignment = NS_DEFAULT_MEMORY_ALIGNMENT);
NS_CORE_KERNEL_API
void* NsRealloc(void* ptr, NsSize size, NsUInt alignment = NS_DEFAULT_MEMORY_ALIGNMENT);
NS_CORE_KERNEL_API
void NsDealloc(void* ptr);
template<class T> T* NsAlloc(NsSize count, NsUInt alignment = NS_DEFAULT_MEMORY_ALIGNMENT);
//@}
These functions offers the same functionality that the standard ones plus the support for requesting a memory alignment. It also exists a templated version of the NsAlloc function to request memory for a specific type of object. Here you specify the number of objects to be created:
struct Data
{
NsInt id;
NsFloat32 value;
};
Data* dataArray = NsAlloc<Data>(10);
This function must only be used for types without contructor and destructor.
AnsiAlloc, AnsiRealloc, AnsiDealloc
These functions define the standard malloc, realloc and free operations, but wrapped to support alignment, and to hide the implementation details for every platform.
Operator new and delete
Global operator new and delete are reimplemented on each compilation unit that define NS_OVERLOAD_OPERATOR_NEW. By default, NoesisEngine packages have it defined.[[BR]]
They use the global functions AnsiAlloc and AnsiDealloc while kernel is not initialized and kernel's memory manager when initialized. Because of this it is not allowed to deallocate memory during kernel lifetime that was allocated before kernel initialization. In the same way, it is not allowed to deallocate memory after kernel shut down that was allocated during kernel lifetime. These two situations will produce a crash.
Components
In a higher layer we define components. Components must inherit from BaseComponent base class. They are objects with a lifetime guided by a reference count policy. That is, you are not responsible for directly deleting the object, only for releasing your reference. It is easier to manage components by using smart pointers, that take care of incrementing and decrementing references for you.
Components are created using the kernel component factory. To create a component you only need to know its class identifier.
ComponentFactory* factory = NsGetKernel()->GetComponentFactory();
Ptr<IStream> stream = factory->CreateComponent(NSS("MemoryStream"));
stream->SetLength(1024);
The reference owned by your Ptr variable is automatically released when the variable goes out of scope.