NoesisGUI

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.

© 2017 Noesis Technologies