interesting - to be even more organized about it:
(
var test = {
|array, weights|
var asig = Signal.newFrom(weights);
bench { 10000.do { |i| array.wchoose(weights) } };
bench { 10000.do { weights.normalizeSum } };
bench { 10000.do { asig.normalize } };
bench { 10000.do { |i| array[i] } };
};
"small: ".postln;
a = (0.0..5.0);
test.(a, a.normalizeSum * 0.9); // 0.9 to avoid fallthrough cases where normalization isn't necessary
"middle: ".postln;a = (0.0..300.0);
test.(a, a.normalizeSum * 0.9);
"large: ".postln;a = (0.0..3000.0);
test.(a, a.normalizeSum * 0.9);
)
I get:
small:
time to run: 0.0017068750000817 seconds.
time to run: 0.002644499999974 seconds.
time to run: 0.00071850000006179 seconds.
time to run: 0.0005948329999228 seconds.
middle:
time to run: 0.0066897919999747 seconds.
time to run: 0.032546832999969 seconds.
time to run: 0.0054264579999881 seconds.
time to run: 0.0005314580000686 seconds.
large:
time to run: 0.050658249999969 seconds.
time to run: 0.24656954199997 seconds.
time to run: 0.048866041999986 seconds.
time to run: 0.00055279200000768 seconds.
Conclusions?
For small arrays, normalizing every time is trivial (comparable to 1 array access / fraction of the wchoose
time. Since normalizing is O(n), for large arrays it’s proportionally the same as the cost of the wchoose
calculation (e.g. 1/6 of the time), but clearly not trivial in the same way.
Thinking about a real-world “bad” case - here’s an example that balances it against the cost of sending an OSC message (which is a small but functionally useful thing you could do, where performance could matter):
(
var test = {
|count, array, weights|
var asig = Signal.newFrom(array);
var addr = NetAddr("127.0.0.1", 12345);
bench {
var values;
count.do {
|i|
addr.sendMsg('/rand', array.wchoose(weights));
};
};
bench {
var values;
count.do {
|i|
asig.normalize;
addr.sendMsg('/rand', array.wchoose(weights));
};
};
};
"real-world: ".postln;a = (0.0..3000.0);
test.(1000, a, a.normalizeSum * 0.99);
)
// time to run: 0.011487041000237 seconds.
// time to run: 0.016208582999752 seconds.
So, we go from 11ms to 16ms for the “convenient” implementation (e.g. normalize by default) - this starts to feel non-trivial. (This is a pretty extreme example, 3000 osc messages per second pulling from a massive weight table - but I guess it’s an open question how much we design for this kind of use specifically.)
I suppose my intuition here is still: someone doing this is writing advanced code, and it seems reasonable to expect that - if they want to incrementally optimize that code - they can add a false
argument to wchoose
. An optional-but-not-neccesary burden on an advanced and uncommon use-case seems better than a functionally-required burden on the majority of simple use-cases. The mistake / oversight case (e.g. wrong/missing flag) for the former is slightly slower performance - for the latter, it’s undefined behavior, a far worse outcome.