In reading through the SynthDef File Format help file, the final section states:
For greatest efficiency:
Unit generators should be listed in an order that permits efficient reuse of connection buffers, which means that a depth first topological sort of the graph is preferable to breadth first.
What does this mean? And how should one practically apply this rule of thumb when making SynthDefs? (Maybe the second question is clear up by the first?)
I’m not an expert here but I don’t think this is relevant for writing ugen graph functions ( SynthDef(key, ugenGraphFunction)) , I think this is talking about the actual files that are going to be sent to the server with doSend, usually generated by the .buildUgenGraph method. incase anyone wants to analyze or generate these directly I imagine.
Semiquaver is right: this is internal to the SynthDef builder. Your code doesn’t have much control over the eventual order.
This advice is meant for developers of other scsynth clients. If that other client implements a SynthDef builder, then it needs to be concerned with this. End users never really need to think about it.
Curious readers could look at topologicalSort defined in SynthDef.
(But, I did recently find a bug in the sort, where a bunch of * operators summed together may result in a wide graph. This, unfortunately, is a common structure in additive synthesis. I’d like to look at fixing it but I’m not sure when I can… well, it’s not a “bug,” precisely, in that the graph will calculate correctly – just not optimally.)
EDIT: It turns out that the topological sort is sensitive to the order in which units were created. In the “ok” case, you get a chain of sinosc → muladd doing the sum. In the “not OK” case, all the SinOscs clump at the top, making a wide tree. So for now, I could recommend using Array.fill with a function to make each parallel chain in full, rather than making a lot of multichannel-expanded units, because topologicalSort ends up not actually sorting it all out.
// ok
(
SynthDef(\addTest, {
var sig = Array.fill(5, { |i| SinOsc.ar(220 * (i+1), mul: Rand(0.5, 1.0)) });
Out.ar(0, sig.sum)
}).dumpUGens;
)
// not OK
(
SynthDef(\addTest, {
var n = 5;
var sig = SinOsc.ar(220 * (1..n));
var amps = Array.fill(n, { Rand(0.5, 1.0) });
Out.ar(0, (sig * amps).sum)
}).dumpUGens;
)
Both orders are valid in the sense that every unit’s descendants all come after that unit.
But… try them both with n = 100 – with default server settings, the first should be ok but the second will fail by exceeding the number of wire buffers.
There’s no reason why it has to overrun the maximum graph width – as the first example shows, they can be arranged in a narrow order. But because of a quirk of the topological sort, it puts all the oscillators at the head (where each one individually needs its own wire buffer).
It can keep going like this indefinitely because it’s just a series of MulAdds with input units.
The other is… I don’t know how to draw it in ASCII but the independent oscillators up front each require a separate wire buffer. This is less efficient because it means more RAM access = less use of CPU cache = slower running.
I’m gonna read up on wire buffers, but if I’m following you correctly:
Ideally the SynthDef graph should have all operations performed on specific a UGen as close as possible to that UGen. And in the case of your example, because the graph is able to flow in the correct order of operations, it is able to compile everything “deeply”, chaining down as your drawing. Versus in your second example, since the oscillators are all clumped up top and the operations that need be performed on them are later, it’s creating several independent chains instead, which is less efficient, and runs into problems at larger scales.
There’s not much documented, because this is pretty deep into the internals and in general, you don’t need to think about it in normal usage.
If you’re not developing a SynthDef builder, then you’re completely free to ignore all of this. (The other aspect is that I may not have time to be a single point of contact for questions on this – curiosity is fine but my time isn’t infinite.)
The rest of that post is correct AFAICS. It’s sometimes not possible to keep UGens very close to their ancestors but in general, it’s better if they are closer rather than farther.
Not to my knowledge. Again, you normally don’t need visibility into this.
I’m making a library of modules for myself, so I go and make incremental efficiency and readability tweaks.
So mostly I want to know what I’m looking for when I use dumpUGens, aside from having the smallest number possible in a way that still does what I want it to do. And this was very helpful for that!