Minor question on CPU efficiency regarding Patterns

Hello again,
This is a pretty “low urgency question”.
I am designing a Pbind that should modulate certain parameters over time according to user input (through midi for example), whilst others stay the same. I have arrived at three different solutions, and I am curious if one is more efficient or preferred in general. These Patterns should be able to evaluate at around 6.000 times per second ideally, possibly even more during performance, which is why the efficiency question is important.
The patterns are wrapped in a class.

//Method 1: Pfunc
MyClass {
	var <>amp
	getPbind {
		^Pbind(
			\amp, Pfunc({amp}),
			\freq, Pseq([220, 330], inf)
		)
	}
}

This is the simplest in syntax, but I think calling the Pfunc so many times per second (also, its many Pfuncs per Pbind in the actual code) is less efficient.

//Method 2: Pdef + Plambda
MyClass {
	var <amp;
	getPattern {
		var freqP = Plet(\melody, Pseq([220, 330], inf), 1);
		var bindP = this.evalPdef;

		^Plambda(
			Ppar([freqP, bindP], inf)
		);
	}

	evalPdef {
		^Pdef(\myPattern,
			Pbind(
				\amp, amp,
				\freq, Pget(\freq, 220, inf)
			)
		);
	}

	amp_ {|val|
		amp = val;
		this.evalPdef;
	}

}

Syntax here is a little more complicated but it seems to me that its more efficient, as we make the most of Pdef’s live EventStream editing functionality. But this may just be from my limited understanding of what sclang and the Patterns interface is doing behind the scenes.

//Method 3: Directly communicating with Synths through Control Busses
MyClass {
	var <amp, ampBus = Bus.control(s);
	getPattern {
		^Pbind(
			\freq, Pseq([220, 330], inf),
			\ampBus, ampBus

		)
	}

	amp_ {|val|
		amp = val;
		ampBus.value_(val)
       //Synths read from this bus at init time
	}
}

This would offload some of the pressure from the client to the server, although the server is also running CPU intensive tasks, so maybe this also isn’t preferred.

Thank you for any insights, and also for all the patience answering these questions!

With modern computers, there’s really no need to worry.

Pfunc { accessAVariable } is definitely fast enough for 6 times per second. You might run into trouble at 60000 or 600000 times per second (which you wouldn’t do, because that’s faster than typical audio rate, let alone control rate).

The second one, I don’t quite understand.

Third option (server-side): Control buses are CPU cheap. Setting a control bus is a microscopic CPU use. In.kr is cheap, as is bus-mapping (see .asMap).

When I started using SC, I’d start hitting CPU limits with 5 or 6 Saw units. Now I don’t hesitate to write 5-6 Saws in a single SynthDef, and play 5-6 note chords using that SynthDef, and the CPU doesn’t even break a sweat. 6 updates per second of a control value will not be noticed.

hjh

Hi, thank you for your answer!

One clarification, its running 6000 times per second, not 6! (My bad, the period is confusing, in spanish speaking countries it is used to separate digits beyond the third).
But I think the answer is roughly the same either way. I will stick with Pfunc({value}) as its the simplest.
I am currently reaching 15% CPU usage on the server, which is fine, but I do want to expand this further so I am trying to limit any extra work for the server, no matter how small.

Ah OK – my own US bias (and I didn’t stop to think why there were three 0s instead of one).

I’m reminded of a comment from James McCartney, a long, long time ago on the mailing list – I forget the exact wording, but the idea was, if the control data require higher bandwidth than the output data, then maybe the control mechanism could be improved. That is, if you put x amount of information in and get y amount of information out, ideally you’d like y > x. If x > y, then you’re putting in a big effort and getting a smaller result, and there’s probably a better way.

Bandwidth of stereo 48kHz, using 32-bit floats, is 48000 * 2 * 4 = 384000 bytes per second.

If you’re creating 6000 synths per second, then the x vs y bandwidh cutoff point is 384000/6000 = 64 bytes per s_new message… which is really not a lot. If synth arg names are all short, you could get five synth args. It sounds like you have more than that.

The CPU overhead of evaluating Pfunc { aVariable } several thousand times per second is minimal. The overhead of creating and destroying a new synth (including initializing every UGen, and releasing UGens’ resources at the end), multiplied by 6000 times per second, is certain to be more than that – if there’s inefficiency to be improved, it’s probably here. That would increase the complexity of the SynthDef, but it’s hard to be more specific about that without knowing what the synths are doing.

hjh

Or… another approach (on the language side) to minimize the overhead of constructing /s_new messages is to manipulate a message template directly.

// control test: how's the performance of a lot of Synth calls, with 10 args each?
s.quit;

(
var a = Pfunc { 1 }.asStream;

bench {
	100000.do {
		Synth(\xyz, [
			a: a.next,
			b: a.next,
			c: a.next,
			d: a.next,
			e: a.next,
			f: a.next,
			g: a.next,
			h: a.next,
			i: a.next,
			j: a.next
		])
	};
};
)

time to run: 2.3657738330003 seconds.

// experimental: mutate a prepared message array
(
var a = Pfunc { 1 }.asStream;
var msg = Synth.basicNew(\xyz, s, 1000).newMsg(args: [
	a: 1,
	b: 1,
	c: 1,
	d: 1,
	e: 1,
	f: 1,
	g: 1,
	h: 1,
	i: 1,
	j: 1
]);

bench {
	100000.do { |i|
		msg[2] = s.nextNodeID;
		(6, 8 .. msg.size-1).do { |i|
			msg[i] = a.next;  // or whichever source
		};
		s.sendMsg(*msg);
	};
};
)

time to run: 1.6584076100007 seconds.

It probably could be even more efficient if you have several parameters that you know will hold steady for several events – like your amp. If you know, for instance, that the index in the message of the amp value is 12, then to change the amplitude, you’d just msg[12] = newAmp and subsequent events we reused the new value without any lookup or re-processing at all. Savings may be considerable for every parameter you can avoid touching per event.

This isn’t a common working method, but it should reduce some overhead.

hjh

1 Like

This is incredibly useful, thank you! I am still very new to SC so all of this helps me not just with this question but with understanding how the whole thing works. The insight on the control data/output data relationship is also very good to think about for all future projects.
I think you’re right with the main issue being on the server. I tried a different approach, with a single synth that retriggers certain envelopes, which did of course reduce CPU usage by a lot, but I didn’t manage to get it to sound how I want. Another thing I haven’t tried is some workaround with the granulator Ugens. I suspect the answer is actually somewhere between “One Synth” and “6000 Synths”.
I will experiment a bit and see if I find a good solution :slight_smile: