19

Now that C# supports named parameters, I was checking to see if it was implemented the same way VB did it, and found that there is a slight difference. Take for example a library function like this:

public static void Foo(string a, string b)
{
    Console.WriteLine(string.Format("a: {0}, b: {1}", a, b));
}

In C#, if you call it like this:

Foo(a: "a", b: "b");

The compiler produces the following IL instructions:

.locals init (
    [0] string CS$0$0000,
    [1] string CS$0$0001)
L_0000: nop 
L_0001: ldstr "a"
L_0006: stloc.0 
L_0007: ldstr "b"
L_000c: stloc.1 
L_000d: ldloc.0 
L_000e: ldloc.1 
L_000f: call void [TestLibrary]TestLibrary.Test::Foo(string, string)
L_0014: nop 
L_0015: ret 

Which translates to the following C# code:

string CS$0$0000 = "a";
string CS$0$0001 = "b";
Test.Foo(CS$0$0000, CS$0$0001);

In VB, if you call it like this:

Foo(a:="a", b:="b")

The compiler produces the following IL instructions:

L_0000: nop 
L_0001: ldstr "a"
L_0006: ldstr "b"
L_000b: call void [TestLibrary]TestLibrary.Test::Foo(string, string)
L_0010: nop 
L_0011: nop 
L_0012: ret 

Which translates to the following VB code:

Foo("a", "b");

The way VB does it requires much fewer instruction calls, so is there any advantage to the way C# implements it? If you don't use the named parameters, C# produces the same thing as VB.


EDIT: Now that we've determined that the extra instructions go away in release mode, is there a particular reason for them to be present in debug mode? VB acts the same in both modes, and C# doesn't insert the extra instructions when calling the method normally without named parameters (including when you use optional parameters).

Greg Shackles
  • 10,009
  • 2
  • 29
  • 35
  • Switch order on the parameters, Foo(b:="b", a:="a") to see the difference (hint: side effect order) – adrianm Feb 25 '10 at 05:11
  • What's the equivalent VB code? – Jon Limjap Feb 25 '10 at 05:15
  • @adrianm an interesting thought, but I've just tried it (out of curiosity) and that generates the same IL as `Foo(a: "a", b: "b");` – Marc Gravell Feb 25 '10 at 05:18
  • Both VB and C# are subject to the same effects from moving parameters around, as far as I can tell. – Greg Shackles Feb 25 '10 at 05:22
  • 2
    @Marc that is probably because strings don't have side effects. If you change them to function calls with side effects (i.e. Foo(b: Method1(), a: Method2())) the release version can't remove the temporaries. – adrianm Feb 25 '10 at 09:09
  • @adrianm That's really interesting, thanks for the example. I tried it out, and while VB still doesn't create the local variables, C# in fact still does even in release mode. It doesn't do that if you don't use named variables though, so maybe this would be a better example for the original question. – Greg Shackles Feb 25 '10 at 12:42
  • 1
    The C# specification says that arguments are evaluated from left to right. VB has no such restrictions. – adrianm Feb 25 '10 at 14:17

2 Answers2

13

is there a particular reason for them to be present in debug mode?

The difference is between:

  • push a temporary value on the stack, use it, discard it, and
  • store a temporary value into a specific stack slot, make a copy of it onto the stack, use it, discard it, but the original copy of the temporary value stays in the stack slot

The visible effect of this difference is that the garbage collector cannot be as aggressive about cleaning up the value. In the first scenario, the value could be collected immediately once the call returns. In the second scenario, the value is only collected after the current method returns (or the slot is re-used).

Making the garbage collector less aggressive often helps in debug scenarios.

The implied question is:

Why the difference between C# and VB?

The C# and VB compilers were written by different people who made different choices about how their respective code generators work.

UPDATE: Re: your comment

In the C# compiler, the unoptimized IL generation is essentially of the same structure as our internal representation of the feature. When we see a named argument:

M(y : Q(), x : R());

where the method is, say

void M(int x, int y) { }

we represent this internally as though you'd written

int ytemp = Q();
int xtemp = R();
M(xtemp, ytemp);

Because we want to preserve the left-to-right evaluation of the side effects of Q and R. This is a reasonable internal representation, and when we codegen it in non-optimized mode, we just codegen the code directly from the internal representation with hardly any modifications.

When we run the optimizer we detect all kinds of stuff -- such as the fact that no one uses those invisible local variables for anything. We can then eliminate the locals from the codegen.

I know very little about the VB internal representation; I haven't worked on the VB compiler since 1995, and I hear that it might have changed just slightly in the last fifteen years. I would imagine that they do something similar, but I don't know the details of how they represent named parameters or how their code generator deals with them.

My point is that this difference does not, to my knowledge, illustrate an important semantic difference. Rather, it illustrates that the nonoptimized build just spits out whatever high-level internal representation we happened to generate that we know has the desired semantics.

Eric Lippert
  • 647,829
  • 179
  • 1,238
  • 2,067
  • 1
    @Eric That's a good explanation, thanks! As for the implied question, I meant it more in terms of "why did they make those choices" since I assumed that there were good reasons for doing so. – Greg Shackles Feb 25 '10 at 16:28
  • +1 for emphasizing the Left-to-Right evaluation which you guys seem to follow religiously in C# to keep it consistent (Also referring to your F() + G() * H() example in another answer) – Michael Stum Feb 26 '10 at 07:11
5

The compiler produces the following C# code:

No - the compiler produces IL, which you are then translating to C#. Any resemblence to C# code is purely accidental (and not all the generated IL can be written as C#). The presence of all those "nop" tells me you're in "debug" mode. I would retry in "release" mode - it can make a big difference to these things.

I fired it up in release mode, using:

static void Main()
{
    Foo(a: "a", b: "b");
}

Giving:

.method private hidebysig static void Main() cil managed
{
    .entrypoint
    .maxstack 8
    L_0000: ldstr "a"
    L_0005: ldstr "b"
    L_000a: call void ConsoleApplication1.Program::Foo(string, string)
    L_000f: ret 
}

So identical.

Marc Gravell
  • 1,026,079
  • 266
  • 2,566
  • 2,900
  • You're right, I had the language backwards there...good catch. I tried it again in release mode (which I thought I had tried, but it would seem I had not). The C# IL instructions match the VB ones in that mode. That still leaves me wondering what the reasoning is for producing the extra instructions in debug mode, since VB seems to do it the shorter way in both modes, but it is good to know it is not an issue in release mode. – Greg Shackles Feb 25 '10 at 05:20
  • @gshackles - I would guess that internally it uses the simplest interpretation available that *guarantees correctness* during the translation stage (i.e. the version in your question), and then relies on later (optional) stages in the compiler to optimise it. Having very literal code translation makes debuggers simpler and more reliable. – Marc Gravell Feb 25 '10 at 05:32