This is called inlining in the interpreter. There is a huge issue about this on GitHub… falling back from inlining · Issue #3567 · supercollider/supercollider · GitHub
Tldr: do not use ugen:if, just do a linear interpolation (pretty sure this works).
(a > 1).linlin(0, 1, false, true);
Essentially, in smalltalk we like to think all ‘things’ are Object, and all we do is send messages. Unfortunately this is horrifically slow. The most beneficial way we can add speed is through inlining, which is copying the definition of the function to where it is called at compile time. This requires two pieces of information at compile time, the class of the objects and the message name.
Here is a hypothetical example.
A {
foo { |a|
^a.bar
}
}
A().foo(1)
Here, the compile knows we are calling *new on A, which return an instance of A, then can see the definition of foo so it can directly emit bytecode that does the following…
1.bar
This is excellent performance wise.
However supercollider’s compiler is quite crude in terms of semantic analysis so doesn’t have the ability to reason about types, so since the beginning, the decision was made to just assume that if you call if on something, it’s a bool. Then the compiler goes ahead and inlines what it guesses is it’s definition - it doesn’t obey extension methods.
Now it only does this if the true and false branches are what it calls ‘inlineable’… Some here will probably describe what these properties are… But they are completely arbitrary and we can make more things inlinable which will improve performance, but randomly break code. It is better to just not know what they are and avoid using ugen:if.
There are many methods that are inlined like this. If, and, or, while, to name but a few… Additionally, you cannot change them with extension methods, and other methods like isNil and === are additionally confusing when you store the object in certain containers.
So just to be clear, the compiler assumes the type and the implementation if you use if (and friends) but can only do the inlining if the true and false branches meet some arbitrary criteria.
I’ve got a proposal based off other’s ideas at the bottom of that long issue that will solve this, until then id strongly recommend not using ugen:if, not defining these methods yourself, and not redefining any ‘core’ methods in extensions.
Side note, things like integer.do do a different type of inlining, where only the extension methods break - unfortunately this doesn’t really work with the simpler control flow methods.
Side side note, if you overload mustBeBooleanError and don’t throw, you get amazing results because it just falls through into the next bytecode.
Side side side note…
There is absolutely no reason why if(..., 1, 2) can’t be inlined, in fact, it’s quite silly that it isn’t considering how simple it would be.