Dispatched if

I repeatedly stumble over the situation where I need to dispatch if to be valid for UGens and numbers.

Like:

f = { |x| if(x) { 8 } { 2 } };
{ f.(LFPulse.ar(200)) }.plot // fails

Do we want to have something like this in common, perhaps?

if_dispatch { |predicate, trueFunc, falseFunc|
		^if(predicate.isKindOf(UGen)) {
			 // if is implemented for ugens, when the functions are evaluated
			if(predicate, trueFunc.value, falseFunc.value)
		} {
			if(predicate, trueFunc, falseFunc)
		}
	}
1 Like

In the UGen case, though, itā€™s necessary to evaluate both branches ahead of time. You canā€™t conditionally evaluate either 8 or 2 per sample; itā€™s necessary to have both 8 and 2 available all the time and select between the two signals.

Thereā€™s already a way to write ā€œevaluate both and selectā€: if without functions.

if(x, 8, 2)

FWIW (personal opinion) I donā€™t write if for UGens anymore because I find it misleading. If I want a crossfade, I write the crossfade explicitly. If I want a Select, I write the Select directly. UGen if may have been a mistake from the outset.

hjh

3 Likes

This would fall into the inlining issue so you couldnā€™t dispatch on it.

These args arenā€™t inlined so dispatch is fine.

It seems I need to explain this in more detail.

Letā€™s say you have a ā€œmath-likeā€ class that holds an instance variable value.
E.g. it has the binary operator pow.
You want to implement the binary operator min for it.

SomeDummy {
	var value;
	*new { |value| ^super.newCopyArgs(value) }
	
	pow { |other| ^this.class.new(pow(value, other)) }
	max { |other| ^this.class.new(if(value > other) { value } { other }) }
}

Now this is all fine, but now assume that you want SomeDummy objects work for UGens and Numbers alike.
With this, we run into a problem, because there are two different syntactic forms for if, depending on the cases.

We can extend the class:

SomeDummy {
	var value;
	*new { |value| ^super.newCopyArgs(value) }
	
	pow { |other| ^this.class.new(pow(value, other)) }
	max { |other| ^this.class.new(this.if_dispatch(value > other) { value } { other }) }
	// dispatch if for UGens
	if_dispatch { |bool, trueFunc, falseFunc|
		^if(bool.isKindOf(UGen)) {
			 // if is implemented for ugens, when the functions are evaluated
			if(bool, trueFunc.value, falseFunc.value)
		} {
			if(bool, trueFunc, falseFunc)
		}
	}
}

But it would be better if we donā€™t implement such a dispatch in every class, but once.

It might be useful to benchmark removing the if inlining optimisation to know if it is really necessary any more. If it is okay without it, then this becomes unnecessary.

Yes, that would be an optimal solution!

It should just be a case of replacing compileIfMsg in lang/LangSource/PyrParseNode.cpp with

void compileIfMsg(PyrCallNodeBase2* node) {
    PyrSlot dummy;

    int numArgs = nodeListLength(node->mArglist);
    PyrParseNode* arg1 = node->mArglist;

    for (; arg1; arg1 = arg1->mNext) {
        COMPILENODE(arg1, &dummy, false);
    }
    compileTail();
    compileOpcode(opSendSpecialMsg, numArgs);
    compileByte(opmIf);
}

I donā€™t have time to check this out in detail unfortunately, but I imagine making the benchmarks will take the longest.

1 Like

ā€¦I had a further think about this over some lunch and I donā€™t think it will be possible as supercollider currently evaluate the blocks when they are inlineable in the parser and we donā€™t have access to this information in the language. Even if we did, weā€™d need to check for it without using if.

What weā€™d want to do is thisā€¦

True {
   if { |t, f| 
      ^if (t.isKindOf(InlineableFunction) and: f.isKindOf(InlineableFunction)) 
          { t.() } 
          { t }
   }
}
False {
   if { |t, f| 
      ^if (t.isKindOf(InlineableFunction) and: f.isKindOf(InlineableFunction)) 
           { f.() } 
           { f }
   }
}

ā€¦ but this calls if recursively and it isnā€™t possible to check for inlineability in the same way as the compiler did.

Ah yes. So probably we have to make it explicit:

// iff and really iff
+ UGen {
    iff { |trueFunc, falseFunc|
         ^Select.perform(this.methodSelectorForRate, 
                      this, 
                      trueFunc.value, 
                      falseFunc.value
          )
    }
}
+ Boolean {
     iff { |trueFunc, falseFunc|
         ^if(this, trueFunc, falseFunc)
    }
}

Just because it is related: falling back from inlining Ā· Issue #3567 Ā· supercollider/supercollider Ā· GitHub

I wonder, shouldnā€™t compileIfMsg check that the first argument is, in fact, a Boolean, and only optimize in that case? (I.e. only Boolean>>if is special cased?)

And also, shouldnā€™t UGen>>if send value to each of its operands? I.e.

+UGen {
	if { |trueBlock falseBlock|
		var trueUgen = trueBlock.value;
		var falseUgen = falseBlock.value;
		^(this * (trueUgen - falseUgen)) + falseUgen;
	}
}

I think then this would work as expected?

Iā€™ve not edited compileIfMsg but with above if definition the below selects appropriately? (The var x; is enough to prevent inliningā€¦)

(LFNoise1.kr(1) > 0).if {
	var x; SinOsc.ar(440, 0)
} {
	Saw.ar(440)
} * 0.1

Best,
R

I see ā€“ thanks for the clarification.

I would suggest to handle this case in the same way that SC handles UGen + number, UGen + array, number + UGen and array + UGen.

The general approach is to use method dispatch to reduce each case to something unambiguous:

  • number + UGen tries to do a numeric +. The primitive fails because the second operand is neither integer nor float.
  • So then it falls back to secondOperand.performBinaryOpOnSimpleNumber('+', this, adverb).
  • ā†’ UGenā€™s performBinaryOpOnSimpleNumber is this.reverseComposeBinaryOp(aSelector, aNumber, adverb).
  • ā†’ UGenā€™s reverseComposeBinaryOp is BinaryOpUGen.new(aSelector, aUGen, this)

ā€¦ and then you have the correct BinaryOpUGen, where the number is the first operand and the UGen is the second.

If there are appropriate performBinaryOpOnSomeDummy and composeXXX methods, then the case could be handled entirely by polymorphism, without any need to mess around with if.

Iā€™d go so far as to say that in a runtime dynamically-typed language like SC, polymorphism is the idiomatic approach to cases like this.

Unfortunately Iā€™m swamped at work and only have time for a 10 minute e-mail ā€“ otherwise Iā€™d develop a demo. But I do think that if this is a case you need, it would be worth studying JMcā€™s code carefully. It is, IMO, elegant enough to make pretty much any other dynamically typed solution look like a hack.

hjh

Not possible, type information isnā€™t available at compile time.

A while ago, @jamshark70, did suggest the possibility of having the compiler emit both the current inlined and the message send, and then create a new bytecode that would perform the check and switch between. This is possible, but a lot of work, and would leave quite a complicated system.

IMO, it might be better to just accept that there are keywords in supercollider.


@julian, in this example,

max { |other| ^this.class.new(if(value > other) { value } { other }) }

Will this work instead?

{ |other| ^this.class.new(value.max(other)) }

As max is defined on AbstractFunction and Number.

Rather than trying to edit if, perhaps the code can be expressed without using if?

Thatā€™s the gist of my post, yes ā€“ James McCartneyā€™s approach, though, would support both aDummy max: aUGen and aUGen max: aDummy. (Itā€™s quite a brilliant design ā€“ Iā€™m not sure if itā€™s fully original or borrowed from SmallTalk ā€“ I only know that it handles a staggering array of cases without a trace of spaghetti coding.)

hjh

The current inlining is sort of an ad hoc implementation of a more general ā€œmost common recipientā€ kind of optimization. The ā€œpureā€ implementation of if is just a message send, and the method dispatch logic takes care of everything (e.g. routes to the True or False class, or something else).

  0   12       PushInstVar 'maybeABooleanOrNot'
  1   A1 00    SendMsg 'if'

The ā€œgeneralā€ optimization would be to check for the most common message recipients, and then inline those methods. The optimization cases we care about are: the first argument to if is True or False and the second and third arguments are Functions. For the classic ā€œif() {} {}ā€ syntax, we know at compile time that the second and third args are functions. And, we know at compile time that True:if and False:if just call these functions. So, the optimized logic might look like this, in pseudocode:

 if (maybeABoolean.isKindOf(True)) { 
   // .. inlined True:if -> inlined trueFunction
 } else if (maybeABoolean.isKindOf(False)) {
   // .. inlined False:if -> inlined falseFunction
 } else {
   // .. normal dispatch, if(maybeABoolean, trueFunction, falseFunction)
 }

We can imagine this sort of optimization for any number of common / internal methods: check for one or two common cases, else fall back to the ā€œslowā€ path. This is exactly what sclang if does now, EXCEPT that it assumes our slow path is always a ā€œNon-Boolean in Testā€ error and sort of hardcodes that logic into the primitives.

So, making this behave in a consistent way (e.g. if really IS just a normal method) only entails adding a fall-thru case to the inlined code, which is itself just a more general and correct way to optimize this kind of case - basically, do the dispatch instead of assuming an error.

Adding the extra two or three bytecode instructions could cause some very small performance degradations. But, thinking about it an alternate way: introducing a general optimization where we can inline method calls where there is a high likelihood of one particular recipient (where if is just one case of thisā€¦) would probably have a large, cross-cutting performance improvements, even if the individual if case was incrementally slower.

If Iā€™m thinking about it right, the performance slowdown would only really come in the form of more memory usage / cache misses: for the 99% case (e.g. we hit our optimization case), the interpreter would execute the same bytecode as it does now - for the 1% case where we donā€™t have a Boolean, we are currently throwing an error - this would not get importantly slower, and in any case incrementally slower performance in an error case is not really important.

Exception being you couldnā€™t override Trueā€™s and Falseā€™s if in an extension, which would be unusualā€¦ so that sounds like a good compromise!

Actually, the override case is totally fine. You can think about it as two levels of inlining. One level inlines the contents of True:if - this is known at compile time, even if you did something wacky like override the default implementation. The second level inlines the func.value inside of the default True:if when we know itā€™s a function (e.g. in case of a function literal in if () {}{}). if you are an edge licker and override the default True:if, you probably lose the one level of inlining, but still get the call site inlining. And, even if you did something weird like subclass Boolean, this would still work as expected, it would just work via the ā€œnormal dispatchā€ fall thru path.
The general form of this optimization would fix the wierd while behaviors in sclang as well.

Sorry, I canā€™t quite follow this.

So by turning the ā€˜not a booleanā€™ error into a dispatch, this worksā€¦

T { if { "meow" } }

if (T()) { 1 } { 2 } == "meow"

ā€¦but I donā€™t understand how the following could be made to work (not that it needs to, its an odd thing to do).

+True {  if { "meow" } }

if (true) { 1 } { 2 } == "meow"

Hereā€™s what Iā€™m thinking (but definitely open to critique if Iā€™m missing something!):

  1. Default case:
True { if { |trueFunc| ^trueFunc.value } }
False { if { |trueFunc, falseFunc| ^falseFunc.value } }

// initial code
result = if (bool) { 1 } { 2 };

// inline #1
// Supposing we treat True/False as the most common cases and inline - we know the exact method implementation to use in our special cases at compile time.
var arg1 = { 1 },
    arg2 = { 2 };
result = switch(
  bool.class, 
  True, { arg1.value },    // inlined from True:if
  False, { arg2.value },   // inlined from False:if
  { bool.if(arg1, arg2) }  // default 
)

// inline #2
// we can inline func.value to the implementation of that func since we know it at compile time (they're literal functions). Note that if trueFunc were passed as an argument, we couldn't inline because we don't know what trueFunc/falseFunc are. Currently we don't inline of the inner functions have var definitions, but there's no reason why we couldn't hoist var defs to the outer function, which would greatly increase how much we are able to inline.
result = switch(
  bool.class, 
  True, { 1 },
  False, { 2 },
  { // default 
    bool.perform(\if, { 1 }, { 2 })
  }
)
  1. Override case
True { if { |trueFunc| ^"meow" } } // lets break control flow....

// initial code
result = if (bool) { 1 } { 2 };

// inline #1
// we still inline True and False cases, but from the method overrides that have been defined
var trueFunc = { 1 },
    falseFunc = { 2 };
result = switch(
  bool.class,
  True, { "meow" },               // inlined from True:if override
  False, { falseFunc.value },     // inlined from False:if
  { bool.if({ 1 }, { 2 }) }       // default 
);

// inline #2
// Same as before, except no inlining for the True case needed
result = switch(
  bool.class,
  True, { "meow" },
  False, { 2 },
  { bool.perform(\if, { 1 }, { 2 }) }   // default 
);

Ah I understand now, thanks!

So weā€™d get rid of the if bytecodes, turn it into a switch, where we lookup True and Falseā€™s if, inline those definitions, and let everything else fall through - nice!

The only question I have left then is whether we need the definition of if while building the class library. This is the part of the VM Iā€™ve looked at least.