Fixed-capacity vector utility class?


#1

Since memory allocations on the heap are not realtime-safe, one must not grow std::vectors (or juce::Arrays) beyond their initial capacity in the audio thread. std::arrays are not always feasible, as they don’t provide erase or push_back functionality due to being fixed-size.

Does JUCE provide a container that provides this functionality, but can’t grow beyond its initial capacity?
There’s boost::container::static_vector<T,Capacity>, but I don’t want to include the boost library in my project.


#2

Yes, the Array class can do that for trivially copyable objects.


#3

How exactly does it work? I looked at the documentation for Array before, but didn’t find any such functionality.


#4

Outside the audio thread you can call Array::ensureStorageAllocated (elements), which will allocate the needed space.

The objects are then copied in place as a memory copy, avoiding allocation if possible.


#5

That doesn’t guarantee I can’t grow the capacity, though, which is something I’m looking for.
In that case, I may as well use an std::vector and call reserve in the constructor.


#6

Can’t you just make sure your size always stays the same via a jassert or something?


#7

Sure, but that would require me to add a jassert(vec.size() < vec.capacity()) before every single call to push_back and similar, which is quite tedious. It would have been cool to have this functionality baked directly into juce::Array, perhaps via a subclass/template parameter, which enables assertions before every allocation if desired.

In the meantime, I’m just very careful when coding to not grow vectors (but everyone knows that saying “I’ll just be careful here” never works out), and use a profiler to find hidden mallocs.


#8

Why not use a HeapBlock that you resize in prepareToPlay() ?


#9

I agree, it’s a pain. But it is the only way IMHO, since if you would force it not to grow, what should be the consequence? Buffer dropout? throw an exception?
The only thing that can work, is to design your code around, so it doesn’t want to grow.


#10

Because a HeapBlock, similar to an std::array, has a fixed size (which equals its capacity) and therefore I can’t dynamically add or remove elements from it.


#11

A simple jassertfalse would be enough for me. This way the operation doesn’t fail, but I am notified that it allocated memory and I should re-think my design.


#12

My practical advice would be to use a normal array, but preallocate enough space that you don’t expect it to ever hit capacity. Then add some checks to assert if it does so you can re-think your reservation size.

The thing is that if you did use a special array that couldn’t reallocate, but you don’t expect that situation to ever really happen, then how does it help? If an overflow means that your code has to fail, then that’s a worse outcome than just using a normal vector which does an allocation and carries on running. Obviously it’s not safe to allocate, but there’s a very high chance that if you do a single emergency allocation, nobody will ever notice.


#13

That’s exactly what I suggested - if the Array could be configured to call jassertfalse when allocating, the program would still run as intended, but I, as the developer, get notified about the issue without having to do overflow checks myself.


#14

Oh right. Hmm, I don’t really see that as a responsibility of the Array - a better approach would probably be to intercept all allocation calls and check whether they’re on the audio thread, which would catch many other more subtle mistakes than just this very specific use-case.


#15

That sounds indeed pretty useful.
Can you point me in the right direction, how to do that?


#16

One thing came to my mind, when investigating options, if Array would allow to query it’s allocated size, you could easily do also the jassert route:

jassert (array.allocatedSpace() >= array.size() + numElementsToAdd);

but intercepting a level below is also a quite nice option.


#17

I’ve implemented a test for this in pluginval and a utility class to go along with it.

The test checks for allocation in the audio thread whilst processing.


#18

Amazing!
I’ll check that out right now.


#19

This is weird - I’ve built an unoptimized version of my plugin in debug mode, including an intentional heap allocation to see if your plugin detects it, but here’s what happens:

As you can see on the left, pluginval says “all tests completed successfully”, while XCode Instruments finds the malloc… (I profiled the execution of pluginval)

I’m using the latest Release (v0.1.4) for macOS from Github.
In headless mode, I get the same result, although the AllocatorInterceptorTests, presumably testing pluginval itself, pass.


#20

That’s odd, if it’s going through operator new it should be caught. Can you tell me what’s doing the allocation? There are a couple of different operator new’s, it might be using a different one.