When are method arguments evaluated in Mono's C#?

Started by
5 comments, last by ChaosEngine 6 years, 11 months ago

Hello all,

First of all, sorry if this is kind of a dumb question, but I can't seem to find an answer for it.

Let's say that in a c# project (in my case, a c# script inside a Unity3d game, if that changes something), I have two methods defined as follows (in pseudo code):


int x;
int y;
​int z;
​int u;
​int v; // these ints are given values depending on the evolution of each game run, so they are not known at compile time.
bool intToBool( int x ) { /*some complex block that computes a bool from the passed argument*/}
int gdc ( int x ) { /* method that takes a int value dependent on the game run 
-that is, not known at compile time- 
and computes another int from it thorugh very complex and expensive calculations*/}

void SomeStuff(int x){} //auxiliar methods called inside myMethod
void OtherStuff(int x){}
void MoreStuff(int x){}

void myMethod(int a, int b, int c, int d, int e){
  /* here goes code that references the arguments by name multiple name, for example:*/
  SomeStuff(a); OtherStuff(b); MoreStuff(c);
  SomeStuff(d); OtherStuff(e); MoreStuff(a);
  SomeStuff(b); OtherStuff(c); MoreStuff(d);
  SomeStuff(e); OtherStuff(a); MoreStuff(b);
  SomeStuff(c); OtherStuff(d); MoreStuff(e);
}
  
 
void myMethodBis(int a, int b, int c, int d, int e){
  if( intToBool( a )) { /*execute statemens*/ }
  else if( intToBool( b )) { /*execute statemens*/ }
  else if( intToBool( x )) { /*execute statemens*/ }
  else if( intToBool( d )) { /*execute statemens*/ }
  else if( intToBool( e )) { /*execute statemens*/ }
}

Further suppose that these methods are going to be called with arguments that are not known at compile time (e.g. because they depend on the evolution of the particular run on the game), like this:


myMethod( gdc(x), gdc(y), gdc(z), gdc(u), gdc(v) );

myMethodBis( gdc(x), gdc(y), gdc(z), gdc(u), gdc(v) );

So, my question is, when are gdc(x)-gdc(v), etc. evaluated?

Are they first computed at the start of the method calls and saved as local variables internally?

Or are they computed each time they are referenced in the method's block?

So, for example, if gdc(x) - gdc(v) are going to be computed regardless of wether they are called in the method's body, and gdc is very computationally intensive, I should rather inline MyMethodBis to save four of these computation.

On the other hand, if they are computed not when the method is invoked but when they are used inside the method body, In myMethod I should first save a-e as local variables so I'm only computing them once, and use the local variables several times through the method's body.

(Bear in mind that the main aim of my question is to know how is the code executed internally, rather than having to do the optimisations I meantion. That has been only the problem that has sparkled the theoretical question).

Thank you in advance for any insight you can give regarding this topic.

Advertisement

So, my question is, when are gdc(x)-gdc(v), etc. evaluated? Are they first computed at the start of the method calls and saved as local variables internally?

It's fully defined in the C# language specification. I grabbed a random version from the Interwebs, it seems old, but such basic concepts never change anyway:

https://msdn.microsoft.com/en-us/library/aa691335(v=vs.71).aspx

During the run-time processing of a function member invocation (Section 7.4.3), the expressions or variable references of an argument list are evaluated in order, from left to right, as follows:

...

So it promises to start with "gdc(x)" and end with "gdc(v)" evaluation (this is relevant if the gcd calls share some data that they modify).

There are lots of complications with reference variables, and null values etc, but that all doesn't apply with "int"

The actual call is defined in Section 7.4.3, apparently, which is
https://msdn.microsoft.com/en-us/library/aa691340(v=vs.71).asp

  • The argument list is evaluated as described in Section 7.4.1.
  • M is invoked.

Again all kinds of cases for all the different kinds of functions, but the above is everywhere in some way. First evaluate the arguments, then call the method.

Of course this makes sense, the function header (technically known as "function signature") says to expect a number of integers, so if you have expressions instead, they first need to be converted to integers.

The above is what any C# compiler will do, at conceptual level. That means, no matter what code you write, or whatever code the compiler actually generates or executes, results that you get must be explainable from the above description. If not, it's a compiler bug.

I deliberately said "whatever code the compiler actually generates or executes" there. The compiler is free to change anything wrt code generation that it likes, except it must result in answers that can be explained from the description in the language specification. The compiler thus may inline your method call, or may conclude that it never needs to evaluate "gcd(v)" in some case. It may also decide to expand "gcd(x)" and compute whenever it is needed. All that and more is allowed, the answer just must be the same as a compiler that literally implemented the language specification.

Why doesn't the language description explain what the compiler really does?

Reality is, compilers change all the time. People are constantly looking for, and finding, new ways to compute things faster. Compilers quite literally not do what they claim to do. They do something else instead, which is generally performing better, but you never know it does something else, since the result cannot be distinguished from the description of the language reference.

This is why the language description is describing the execution at conceptual level. For a programmer, it's a solid foundation. If you follow the rules of the language specification, your program will now and in the future continue to work, no matter what weird tricks the compiler authors pull from their hat.

For the compiler authors, it gives freedom. They can change anything they like, as long as the result with respect to the language specification remains the same. All programs ever written will continue to work.

So the answer to your question is "don't know, and that's good". Tomorrows compiler may do things differently anyway, and that's fine.

[...]

Thank you very much for the answer! Now that begs new questions, like how come operators can be "shortcut" like && (i.e. operands are not evaluated before the body of the operator is executed) and methods seemingly can not. But then again my internal knowledge of c# operators themselves is lacking. I'll have to deep into the language specification further!

how come operators can be "shortcut" like && (i.e. operands are not evaluated before the body of the operator is executed) and methods seemingly can not

Why would you say that?

Parameters are different than operators. The && operator is evaluated first, returning true or false, and that true or false is evaluated as a parameter. To evaluate && it looks at the first, then looks at the second.

Short-circuit evaluation is only available for relevant boolean operators, && and || (the XOR, ^, operator isn't, since you always need to evaluate both operands anyway). You could say those operators are somewhat 'special' yes, but is definitely different from what you're expecting. The conditional operators can easily be implemented within the language. For example you can have:


if (condition && otherCondition)

changed to


if (condition)
{
    if (otherCondition)
    {

which makes sense, because at run time you'll know the value of the first argument and based on that can determine whether you need to determine the second at all. Having to write those if-statements in all scenarios would become a little tiring though!

The evaluation process you describe is vastly different however. You basically want to pass the argument, which is the result of a function, and only afterwards once you have entered the function and determined you don't need that value that you asked C# to compute for you upon entering, decided to not compute it after all. More importantly, what if gdc had side-effects such as printing something as console output? You called the function, so you'd at least expect the result to show up at the time of calling it! Such side-effects make something alike near impossible and so is deciding in advance whether a function will have side-effects. However, this isn't a problem for either of the boolean operators.

What you're looking for is called lazy evaluation. There's at least one language I know supporting it, which is Haskell, a functional programming language without side-effects, making things quite a bit easier ;)

For this case, I'd simply try to modify the structure so that the function you're calling instead computes gdc and you pass the arguments you would have passed to gdc. That way, only if the function you call evaluates the arguments, will gdc be computed. Of course storing/caching the results of particular calls to gdc would help too, if performance really is a problem.

Now that begs new questions, like how come operators can be "shortcut" like && (i.e. operands are not evaluated before the body of the operator is executed) and methods seemingly can not.

You're using knowledge about the function that in general doesn't exist, or at least, you cannot specify it as a single function.


real f(int a, int b, int c) { .. }

quite literally states "I am function f, I need three integers, and return a real to you".

It doesn't state "I need a, and b when it's in the afternoon, and c if a is more than 5 and it is high tide". The C# language also doesn't provide any means to state these things as part of the function f.

If you really want, you can code this of course, by making functions f1..f5, testing the usage conditions outside the function, and then call the correct f_i variation. At that point however, it's not language specification any more, but a thing coded by the programmer (user of C#).

The standard math (ie what mathematicians use) "and" operation is not short-circuit. It evaluates both sides, and then computes the overall result, just like any other binary operator, like + or *. This kind of "and" however is very annoying in programming.

Imagine you have to check for two conditions:

1) list may not be empty

2) first element must be at least 5.

With math "and", mylist.size() != 0 "and" mylist[0] == 5 fails. For the empty list, "mylist[0] == 5" cannot be computed and it throws an exception OutOfRangeError or so. The reason is that math-and first evaluates both sides, and then computes the overall result.

The computer-and && is specifically designed to avoid such problems. mylist.size() != 0 && mylist[0] == 5 works, because the && doesn't even consider the second operand if the first one doesn't hold. You should be able to find this in the evaluation of the && operator.

In addition to what others have said, let's say you have a method that looks kinda like this. waaay over-simplified, but bear with me.


int DoStuff(bool b, int x, int y)
{
   if (b)
     return x;
   return y;
}

Pretty simple, right? Now let's assume that you're calling it like this


int x = SomeReallySlowFunction();
int y = AnotherReallySlowFunction();

int answer = DoStuff(someBoolValue, x, y);

Uggh, we've called both slow functions and we really only need one! Well, we could move the logic out of DoStuff, but let's assume that DoStuff is actually a bit more complicated and we don't want to do that.

What are our options?

Well, you could put the parameters into a class with lazy evaluated properties, but that's kind of a pain.

But the other option is to pass delegates instead of values, i.e.


int DoStuff(bool b, Func<int> getX, Func<int> getY)
{
   if (b)
      return getX();
   return getY();
}

// called like this
int answer = DoStuff(someBoolValue, SomeReallySlowFunction, AnotherReallySlowFunction); 

Note the lack of () on the two method calls. Now, we are only calling whatever methods we actually need.

That said, passing a whole bunch of parameters into a method that probably won't need them is usually a code smell and an indication that you should refactor your method.

if you think programming is like sex, you probably haven't done much of either.-------------- - capn_midnight

This topic is closed to new replies.

Advertisement