Memory Management

Introduction

DD::Image::Memory implements a Memory management system providing a shared interface to monitor allocations and deallocations, callbacks for memory information, and adherence to the memory limit set in Nuke’s Preferences.

There are three fundamental parts to the Memory system that plug-in developers should use in order to achieve this:

  • Using DD:Image::Memory’s allocate and deallocate functions
    • Provides a thread-safe mechanism for allocations that also tracks Nuke’s overall memory usage, as well as attempting to free memory when the user-defined limit is reached
  • Registering MemoryHolder objects
    • Allows for objects to receive callbacks for providing extra information on how much memory it’s using and callbacks for freeing memory.
  • Allocators
    • Provides more generalized memory usage information, can be specialized to provide performance increases and provide a simpler way to track memory for info callbacks.

The sections below describe each of these in more detail, including examples of how to use them.

allocate_void/deallocate_void Functions

At the core of the Memory system are allocate_void(size_t) and deallocate_void(void*), which internally do a couple of things.

  • Guaranteed 16-byte alignment cross-platform
  • Before allocating any memory, checks that it’s not about to go above the maximum usage
    • If it is about to go over the memory limit, all registered MemoryHolder objects are sorted by weight and their free callbacks are executed until the memory is sufficiently lowered, or it runs out of registered users.
  • The Memory system then attempts to allocate memory, and if it fails, then it attempts to reduce the memory usage and again tries to allocate, throwing a bad_alloc() exception if no memory can be allocated.

It’s important to note that the current usage reported by DD::Image::Memory may not be the same total as the amount requested. This is because we guarantee 16-byte alignment on all platforms (this is already the case on OSX and Windows), but can also depend on how the OS allocates the memory requested*. It’s this system memory amount that DD::Image uses to determine the current usage, which can be different depending on platform.

For example, on Snow Leopard if you allocate 1040 bytes you actually get 1152 bytes allocated by the system. This is due to how OSX implemented its memory allocations into particular sizes, see http://www.opensource.apple.com/source/Libc/Libc-594.1.4/gen/magazine_malloc.c for its implementation

MemoryHolder

To register an object that allocates memory, you are required to implement three pure abstract functions:

  • bool memoryFree( size_t amount ): Called when the memory limit is reached, the user should try and free as much memory as possible so as to allow other operations to use the memory as needed. The amount argument is how much Nuke needs to free in order to get within the memory limit set. Return true if any memory has been freed.

    Note: It’s important to be aware that memoryFree() could be called from within an allocate() call. This is especially important when using locks to ensure other threads aren’t currently using the memory about to be freed. The recommended solution is to only try to lock and if it fails then return.

  • int memoryWeight(): This is used internally to sort the registered objects, sorting by smallest to largest. Returning a large weighting makes it less likely that free calls will be made on it, is used in caching to weight higher based on number of requests.

  • void memoryInfo( Memory::MemoryInfoArray& output, const void* restrict_to ): Provides a mechanism to report how much memory a particular object is using. A user should add a MemoryInfo instance to the vector passed in (this is so a MemoryHolder can report info on more than one instance).

    A MemoryInfo class contains a total usage amount, an owner type (used to get the name of the object that it’s associated with), and an array of key value pairs to add any extra user information.

Allocators

In addition to the MemoryHolder interface, DD::Image also provides an API for registering Allocators, as well as providing two example allocators used within Nuke.

There are several benefits from using an Allocator:

  • Can be customized for a particular allocation pattern
  • Used to accurately track STL memory usage, (use provided STL macros mFnCreateInstanceSTLAllocator and mFnCreateGlobalSTLAllocator for creating wrapper STL interface classes)
  • Can use as means to track memory for MemoryHolder mentioned above, interface also includes API calls to get high watermark usage.

Creating and registering allocators

To create an allocator and register it with Nuke, create an instance of one of the allocators available in DD::Image, or create a class that implements the IAllocator interface available in DD::Image, and then call register_allocator() to register it.

An allocator cannot be registered more than once, and should be uniquely named. It should also allocate all memory through the allocate_void/deallocate_void calls in DD::Image, so that Nuke can attempt to adhere to the memory limit set.

Heap and Block allocators

Included in DD::Image are two allocators that can be instanced and used by plug-in developers. The relative benefits are described below.

HeapAllocator

This is a simple, fast, thread-safe allocator that tracks current usage and high watermark levels per instance.

BlockAllocator

This is a thread-safe allocator that, for a customized range of allocation sizes, creates blocks of a given size and allocated memory from these “buckets”.

Block allocation also reduces the amount of fragmentation and number of system memory calls, at the expense of a memory overhead. It’s used within the 3D system for this exact reason, where lots of small allocations are made.

Table Of Contents

Previous topic

Op Hashing & Caching

Next topic

Curve serialisation format