Efficient modular audio processing?

DSP, Plugin and Host development discussion.
RELATED
PRODUCTS

Post

Hello,

i'm trying to figure out how to arrange the audio processing in a plugin more efficiently.

A somewhat naive but fairly efficient way would use something like:

Code: Select all

struct Processor {
    virtual void process(float* buf, int numFrames) = 0;
};

struct Gain : public Processor {
    virtual void process(float* buf, int numFrames) {
        input->process(buf, numFrames);
        for (int i=0; i<numFrames; i++) {
            buf[i] *= gain;
        }
    }
    Processor* input;
    float gain;
};
The process forms a tree where each node implements Processor. The good thing about this is that it is modular and easy to understand and the signalpath and processing order is easy to identify. I suppose that's just about it about the benefits.

Now i know two ways to try to do this more efficiently. We could try to get rid of the nested virtual function calls and we could process multiple channels in parallel. Are there any other issues?

There is some previous discussion about the subject, but as usual it is impossible to estimate the significance of any given method, except after actually implementing and measuring. I would like to know if you have any thoughts, experience or advice?

Post

I have used something similar a few times before and it worked fine. It was easy to keep track of the audio flow and quick to code.

Also once you start doing something more complex to your buffer than just applying gain then the cost of the virtual functions diminishes too, and the larger the buffers you use then the less impact your virtual call has too. From memory I think I found 32 samples to be a good trade off between buffersize / CPU usage for me.

Post

Caco wrote:I have used something similar a few times before and it worked fine. It was easy to keep track of the audio flow and quick to code.
Yes, i think it has it's benefits and it's "user friendly" especially from the coder's point of view. That's kind of why i'd like to know more before refactoring to some function pointer array thing and breaking all that's good in it.
Also once you start doing something more complex to your buffer than just applying gain then the cost of the virtual functions diminishes too, and the larger the buffers you use then the less impact your virtual call has too. From memory I think I found 32 samples to be a good trade off between buffersize / CPU usage for me.
That's good to know, i've sort of seen the same effect with buffer sizes. The nested function calls presents another issue. By intuition i'd say that a depth in the order of hundreds isn't that horrible and the fact that it's not per sample applies here too.

Post

If you are not doing complex inheritance you can use the CRRTP to achieve "compile time polymorphism", provided your compiler does not virtual function optimization.

The cost is that the code is less clear, up to you to decide if it's worth or not.

Post

rafa1981 wrote:If you are not doing complex inheritance you can use the CRRTP to achieve "compile time polymorphism", provided your compiler does not virtual function optimization.

The cost is that the code is less clear, up to you to decide if it's worth or not.
Oh yes, thanks! This is certainly better than going with raw function pointers. Code reuse through inheritance is a bit difficult with CRTP or am i missing something? I mean we can't really override the "virtual" function in any derived class or can we?

Edit:
Does compiler optimization perform better without virtual functions? That could make it worth while even if the overhead by virtual function calls is not that bad by it self.

Post

AUTO-ADMIN: Non-MP3, WAV, OGG, SoundCloud, YouTube, Vimeo, Twitter and Facebook links in this post have been protected automatically. Once the member reaches 5 posts the links will function as normal.
Here a nice paper about this topic:

Authors: Arumi, P. Garcia, D. Amatriain, X. (2006)
Title:"A Dataflow Pattern Language for Sound and Music Computing"
In: Proceedings of Pattern Languages of Programming (PloP 06).
Year: 2006

You can find it here http://xavier.amatriain.net (http://xavier.amatriain.net)

Post

zman wrote:Here a nice paper about this topic:

Authors: Arumi, P. Garcia, D. Amatriain, X. (2006)
Title:"A Dataflow Pattern Language for Sound and Music Computing"
In: Proceedings of Pattern Languages of Programming (PloP 06).
Year: 2006

You can find it here http://xavier.amatriain.net
Thank you! Very interesting paper. I already found some useful patterns after a quick look. Unfortunately, being a bit academic and high level, it doesn't seem to answer much of my questions.

Post

I'm doing exactly this in my iPad synth and a plugin I'm working on. By my measurements the overhead of the virtual function calls disappears at about 16-32 samples in a block. Processing samples in blocks has other upsides too. For example, it makes it easier to invoke block-oriented SIMD functions on your data.

Post

You might find counting a loop backwards is faster because repeated comparison to zero is cheaper than loading a constant..

for (int i=numFrames; i> 0; --i) {

..you might find that pre-decrement (--i) is faster than 'post' (i--) because post-decrement uses an additional temporary variable (it returns the value before it was inremented/decremented).
Of course it all depends on you compiler, which may perform these optimisations automatically. Sure it seems a very small thing, but this type of loop occurs thousands of times in audio software, you may as well use the best-of-breed.

Post

mfa wrote:Oh yes, thanks! This is certainly better than going with raw function pointers.
Note that a virtual function call is 'underneath' a raw function pointer, so switching to a more complicated scheme might not help.

Post

Jeff McClintock wrote:You might find XYZ is faster...
Or you can take a smoke break instead, and when you come back to your computer, CPU upgrades taken by your customer base will have caused the average machine to get that much faster without you lifting a finger.
Image
Don't do it my way.

Post

My customer base frequently comes up with computers you wouldn't think possible (such as ancient laptops running Windows 98 SE), so trying to optimize such little things is never a bad idea - it's not the fastest computer, it's the other end of the spectrum that benefits most from it.
"Until you spread your wings, you'll have no idea how far you can walk." Image

Post

Jeff McClintock wrote:You might find counting a loop backwards is faster because repeated comparison to zero is cheaper than loading a constant..

for (int i=numFrames; i> 0; --i) {

..you might find that pre-decrement (--i) is faster than 'post' (i--) because post-decrement uses an additional temporary variable (it returns the value before it was inremented/decremented).
Of course it all depends on you compiler, which may perform these optimisations automatically. Sure it seems a very small thing, but this type of loop occurs thousands of times in audio software, you may as well use the best-of-breed.
How about using a do . . .while loop, that way it won't have to return to the top to do the check, then jump over the code if finished.
I'll get my coat... :)

Post

I find optimizing mostly quite frustrating. The damn compiler does so good job at it, that it's really hard to do anything clever to actually improve performance. :P Everything turns totally counterintuitive after the optimizer is let loose. Reverting to some efficient looking C style coding seems to have no effect on performance. I'm pretty sure the compiler treats i++ as ++i automatically whenever it can. Most attempts to do some clever coding makes the performance worse. :? I assume the weird optimization attempts manage only to confuse the optimizer.

Now i don't really know how optimizers work. I'm just a guy whose been observing them doing their work for a few months. Now what i'm going to say might be total nonsense, so if that doesn't scare you too much, please read further. :)

The optimizer runs at link time and does further optimization that isn't possible within a single compilation unit. At link time almost any piece of code can be inlined and recompiled to increase performance. I would think that code like

Code: Select all

processor->process(buf, numFrames);
where processor is a pointer to a base class and process is virtual, pretty much prevents all build time inference. But maybe the compiler is smart enough to recognize the cases where processor is set only once during initialization and is effectively a constant at run time. If it's not smart enough, i would think that something like CRTP might actually make a big difference. And it would have little to do with the overhead caused by virtual functions. It would just make the optimizer happier.

Post

Borogove wrote:
Jeff McClintock wrote:You might find XYZ is faster...
Or you can take a smoke break instead, and when you come back to your computer, CPU upgrades taken by your customer base will have caused the average machine to get that much faster without you lifting a finger.


:) I actually believe this is true.

Post Reply

Return to “DSP and Plugin Development”