Memory allocation strategies in UGens

Hello everyone

I am trying to figure out good strategies for flexibly allocating buffers in c++ in UGens and I am very interested in hearing your perspectives. Any advice is appreciated!

My problem is I want to create a/some headerfiles at the root of my ugen repo containing common algorithms that I want to combine in different ways inside my ugens, eg. allpass and comb. Some of these rely on buffer allocation of course and I would like to handle this in a sleek and flexible way of course but it is not so straight forward I find. The problem though is that if you do this in a headerfile, then the class in that header file needs access to the ugen’s World and function table it seems. Eg. a constructor like this in a header file:

  // Constructor
  AllPass_Base(World * parent_world, float sampleRate, float maxdelaylength) {
    m_samplerate = sampleRate;

    // Calculate size of buffers
    m_bufsize = NEXTPOWEROFTWO(m_samplerate * maxdelaylength);
    m_mask = m_bufsize - 1; // Used for wrapping purposes

    // Allocate buffers
    m_delaybuffer = (float *)RTAlloc(parent_world, m_bufsize * sizeof(float));
    m_historybuffer = (float *)RTAlloc(parent_world, m_bufsize * sizeof(float));

    // Zero out newly made buffers
    memset(m_delaybuffer, 0, m_bufsize * sizeof(float));
    memset(m_historybuffer, 0, m_bufsize * sizeof(float));

  };

This leads to compilation errors complaining use of undeclared identifier 'ft'.

using RTAlloc in a constructor

What I have done so far is this, where I allocate the buffer in a self contained UGen class in the more canonical way. This seems to be the classic strategy which I have so far used in my humble UGens and it works nicely by itself but the problem, as mentioned above, is how to abstract this in a reusable way in other cpp classes:

// Irate
  maxdelay = in0(3); // Seconds
  bufsize = NEXTPOWEROFTWO((float)sampleRate() * maxdelay);

  // Don't forget to free this in the destructor
  delaybuffer = (float *)RTAlloc(mWorld, bufsize * sizeof(float));
  historybuffer = (float *)RTAlloc(mWorld, bufsize * sizeof(float));


  // This check makes sure that RTAlloc succeeded. (It might fail if there's
  // not enough memory.) If you don't do this check properly then YOU CAN
  // CRASH THE SERVER! A lot of ugens in core and sc3-plugins fail to do this.
  // Don't follow their example.
  if (delaybuffer == NULL || historybuffer == NULL) {

    mCalcFunc = make_calc_function<MKAllpass, &MKAllpass::clear>();

    // Clear outputs of the ugen, just in case
    clear(1);

    if (mWorld->mVerbosity > -2) {
      Print("Failed to allocate memory for MKAllpass ugen.\n");
    }

    return;
  }

  // Fill the buffer with zeros.
  memset(delaybuffer, 0, bufsize * sizeof(float));
  memset(historybuffer, 0, bufsize * sizeof(float));

Using STL containers ?

And alternative to this is to use STL containers. This was recommended to me by a colleague, but then the SuperCollider docs explicitly warn against this:

Memory Allocation
Do not allocate memory from the OS via malloc / free or new/ delete. Instead you should use the real-time memory allocator via RTAlloc / RTFree.
STL Containers
It is generally not recommended to use STL containers, since they internally allocate memory. The only way the STL containers can be used is by providing an Allocator, which maps to the allocating functions of the server.

The NHHall way

Then there’s a slightly different strategy which is seen in the NHHall ugen. This is pretty close to what I would like to do

2 Likes

Currently, my strategy for handling this is pretty similar to how I did it in NHHall. In audio applications, plugin library code should never allocate memory for the user, and always allow them to handle memory allocation themselves.

In NHHall I used a template so users specify their own Allocator class. I’m not wild about that solution anymore and I think it’s a little overwrought, though. These days I use a simpler method where the plugin is initialized in stages:

MyCoolPlugin unit(48000);
size_t bufferSize = unit.getBufferSize();
void* buffer = SpecializedMemoryAllocationRoutine(bufferSize);
unit.initialize(buffer);

On the plugin side, the code would look something like this:

class MyCoolPlugin
{
public:
    // Constructor sets parameters but doesn't allocate memory.
    MyCoolPlugin(float sampleRate);

    // Return the amount of memory needed for this plugin.
    size_t getBufferSize() {
        return getMemoryNeededForAllpass() + getMemoryNeededForDelay();
    }

    // At the "initialize" stage, the callee passes in the buffer of memory.
    // This buffer is sublet into the memory needed for individual sub-plugins.
    void initialize(void* buffer) {
        // probably good to check here that buffer is aligned
        mBuffer = buffer;
        mAllpassMemory = mBuffer;
        mDelayMemory = mBuffer + getMemoryNeededForAllpass();
    }
}

This is a somewhat unsafe and C-like approach to this problem, admittedly. Anyone who’s good at C++ should feel free to chime in with solutions with better compile-time checks.

Regarding your first example: I’ve been in a similar situation (a RT safe std::make_shared replacement, see https://git.iem.at/pd/vstplugin/-/blob/master/sc/src/rt_shared_ptr.hpp) and one possible solution is to make an extern forward declaration of the interface table, i.e. extern InterfaceTable *ft;. In the actual .cpp file of your plugin, you must define the interface table as non-static, i.e. InterfaceTable *ft; instead of static InterfaceTable *ft. Since the plugin build system should only export symbols which are explicitly marked as SC_API_EXPORT, this is not a problem.

The same technique can be used for custom RT allocators (see the linked code), but honestly, for simple containers with a clear lifetime, like the buffers in your example, custom allocators are probably overkill. Note that such allocators always have to be stateful (store a reference to the corresponding World), so every container using this allocator needs extra 8 bytes (on a 64-bit system). Writing a custom allocator just for std::vector would be over-engineering, because dynamic arrays can be easily managed manually. In VSTPlugin, for example, I only use my custom allocator for my shared_ptr replacement.

2 Likes

For the first example problem I have used what Spacechild1 mentions:

Thanks a lot for the inspiration everyone! I will mess around with your suggestions and inspirations in the coming days and see what works for me.

Alright, after messing around with this today I did go with this solution which allowed me to pass a world to my allpass header file’s classes. Seems to work quite nicely!

That said, I don’t think I actually understand what’s going on with this line of code yet.

in my plugin’s cpp file I wrote InterfaceTable *ft and in my external allpass header file extern InterfaceTable *ft;. As I said, it worked out nicely and allowed me to pass the world stuff to my header file’s classes but what does this trick do again? Not sure I 100 % understand it yet

extern InterfaceTable *ft is just a forward declaration of a global variable.

Usually, header files only contain forward declarations of functions - which are extern by default. However, forward declaration of global variables requires the extern keyword. Omitting the extern keyword, e.g. InterfaceTable *ft, would create a variable definition. You can have as many forward declarations as you like, but only a single definition (otherwise you get a linker error because of duplicate symbols).

I admit extern can be a bit confusing, but luckily we rarely use global variables these days - and when we do, we disguise them as “Singletons” :stuck_out_tongue:

2 Likes

@Spacechild1’s explanation is great. Linkage is (in my experience at least) one of the harder concepts to understand in C and C++. It’s partially because the rules determining what type of linkage a thing has are really complicated, and also because some of the keywords that affect linkage – extern, static, const – can have very different meanings depending on where they occur. For anyone who wants to learn more, here is a decent introduction to linkage and how it works: http://www.goldsborough.me/c/c++/linker/2016/03/30/19-34-25-internal_and_external_linkage_in_c++/

Also, in C++20 we now have “modules” (https://accu.org/journals/overload/28/159/sidwell/), which are an entirely new, and hopefully better, way of separating interfaces from implementations. They come with their own keywords (import/export) and symbol visibility rules. I haven’t tried them out yet myself, and they aren’t very well supported by compilers or build systems, but who knows, they might change things up in the next couple years. (Sorry for going off topic here, maybe we should start a new thread if people want to discuss it more)

Also

In SC_InterfaceTable.h we have

#define RTAlloc (*ft->fRTAlloc)

so that RTAlloc needs to have ft declared and defined elsewere. It is defined in the cpp plugin file and it takes its value in PluginLoad.

PluginLoad(BinaryOp) {
    ft = inTable;

    DefineSimpleUnit(BinaryOpUGen);
}

Which is difficult to understand because it is not a function but a macro (also found in In SC_InterfaceTable.h) that results in

void load(InterfaceTable* inTable)
 {
    ft = inTable;

    DefineSimpleUnit(BinaryOpUGen);
}

Then, as said before, it needs to be declared but not defined again ( extern InterfaceTable *ft) in other files that need to use RTAlloc or related functions (your header file for example)

wow that is a really nice explanation. Thanks a lot!

Hi! This seems like an old thread but anyway… Recently i did some tests with replacing the std allocator in order to be able to use std containers in UGens with RTAlloc. Based on the code offered on Allocators | Microsoft Learn
I wrote a wrapper class for RTAlloc and everything seems to work decently GitHub - aleksandarkoruga/testallocator: Some tests for replacing the std allocator with RTAlloc in SuperCollider UGens
The only problem remains for member variables of external classes which use std containers, those are still allocated with the std allocator.
That said, despite the warnings, i have been using std containers, dlls and even the whole OpenGL library from inside UGens for years and have never had a single issue.