Compiler Trick?

B

Brian Tyler

I have seen a very strange piece of code being generated by the C# compiler
and was hoping someone might be able to shed some light on it. I've reduced
the example down to a bare minimum and I am using the .NET 1.1 framework.
Here is the C# code:

using System;

namespace Example
{
class SwitchTest
{
static void Main(string[] args)
{
switch(args[0])
{
case "A":
break;

case "B":
break;
}
}
}
}

And this is the IL generated (pretty much the same for debug and release):

..method private hidebysig static void Main(string[] args) cil managed
{
.entrypoint
// Code size 43 (0x2b)
.maxstack 2
.locals init (string V_0)
IL_0000: ldstr "A"
IL_0005: ldstr "B"
IL_000a: leave.s IL_000c
IL_000c: ldarg.0
IL_000d: ldc.i4.0
IL_000e: ldelem.ref
IL_000f: dup
IL_0010: stloc.0
IL_0011: brfalse.s IL_002a
IL_0013: ldloc.0
IL_0014: call string [mscorlib]System.String::IsInterned(string)
IL_0019: stloc.0
IL_001a: ldloc.0
IL_001b: ldstr "A"
IL_0020: beq.s IL_002a
IL_0022: ldloc.0
IL_0023: ldstr "B"
IL_0028: beq.s IL_002a
IL_002a: ret
} // end of method SwitchTest::Main


QUESTION: What is the purpose of the two ldstr for the case statement
values? The LEAVE instruction clears out the eval stack so they aren't used
elsewhere. I thought at first that it might have something to do with the
Interned string cache, but these are constants and so should already be in
the cache.

Any ideas?

Brian
(e-mail address removed)
 
P

Peter Koen

I have seen a very strange piece of code being generated by the C#
compiler and was hoping someone might be able to shed some light on
it. I've reduced the example down to a bare minimum and I am using the
.NET 1.1 framework. Here is the C# code:

using System;

namespace Example
{
class SwitchTest
{
static void Main(string[] args)
{
switch(args[0])
{
case "A":
break;

case "B":
break;
}
}
}
}

And this is the IL generated (pretty much the same for debug and
release):

.method private hidebysig static void Main(string[] args) cil managed
{
.entrypoint
// Code size 43 (0x2b)
.maxstack 2
.locals init (string V_0)
IL_0000: ldstr "A"
IL_0005: ldstr "B"
IL_000a: leave.s IL_000c
IL_000c: ldarg.0
IL_000d: ldc.i4.0
IL_000e: ldelem.ref
IL_000f: dup
IL_0010: stloc.0
IL_0011: brfalse.s IL_002a
IL_0013: ldloc.0
IL_0014: call string
[mscorlib]System.String::IsInterned(string) IL_0019: stloc.0
IL_001a: ldloc.0
IL_001b: ldstr "A"
IL_0020: beq.s IL_002a
IL_0022: ldloc.0
IL_0023: ldstr "B"
IL_0028: beq.s IL_002a
IL_002a: ret
} // end of method SwitchTest::Main


QUESTION: What is the purpose of the two ldstr for the case statement
values? The LEAVE instruction clears out the eval stack so they aren't
used elsewhere. I thought at first that it might have something to do
with the Interned string cache, but these are constants and so should
already be in the cache.

Any ideas?

Brian
(e-mail address removed)

Hello Brian,


I'll try to explain this line by line (mainly not for you, since you seem
to know IL, but for the other readers of this newsgroup):
IL_0000: ldstr "A"
IL_0005: ldstr "B"

A and B are now loaded on the execution stack
[A] <--- stack, first in is left.

opcodes:
0x72 + token --> 1 byte + token: 4 byte index into the string pool
0x72 + token
"token is a token of a user-defined string, whose RID portion is actually
an offset in the #US blob stream

This is used to see if all values that will be tested on the switch
statement comply with the switch type. if one of them wouldn't be a valid
unicode string the ldstr would fail and an exception would be thrown.
this is far more effective than calling some code to check the type. this
exception will be handeld internaly in the framework as a first-chance
exception and will not be propagated to the user.

total: 10 bytes
IL_000a: leave.s IL_000c
clear the stack and branch x bytes, in this case a lable is used for
calculating the branch step

The leave instruction, or its short-parameter form, is used to exit a
guarded block (a try block) or an exception handler block. You cannot use
this instruction, however, to exit a filter, finally, or fault block.
So this leave.s will (if there has not been a first chance exception that
imposed a fault block) step out of the try. now we are sure that there
are only valid types for the case labels.

[]

opcode: 0xDD + 1 byte offset --> 2 bytes

total: 12 bytes
IL_000c: ldarg.0
load first argument, that's the array reference

[array reference]

opcode: 0x02 --> 1 byte

total: 13 bytes
IL_000d: ldc.i4.0
load constant 0 as int32

[array reference][0]

opcode: 0x16 --> 1 byte

total: 14 bytes
IL_000e: ldelem.ref
load the indexed item from the referenced array

[item]

opcode: 0x9A --> 1 byte

total: 15bytes

IL_000f: dup
duplicates the item on the stack

[item][item]

opcode: 0x25 --> 1byte

total: 16bytes
IL_0010: stloc.0
stores topmost item into first local var

[item]

opcode: 0x0A --> 1byte

total: 17 bytes

IL_0011: brfalse.s IL_002a
branche if item on stack (reference to a string ) is zero.

therefore: if the item wasn't a string it branches to the part of the
code where the strings get loaded

[]

opcode: 0x2C + 1 byte --> 2byte

total: 19bytes
IL_0013: ldloc.0
load the reference back from the local if there was a valid reference
(you might think this is redundant, but it's necessary: just think about
the execution stack: there would be 1 more item after the branch if this
one hadn't been removed before to the local variable)

[item]

opcode: 0x06 --> 1 byte

total: 20 bytes
IL_0014: call string
[mscorlib]System.String::IsInterned(string)

call on Method IsInterned.
The common language runtime automatically maintains a table, called the
"intern pool", which contains a single instance of each unique literal
string constant declared in a program, as well as any unique instance of
String you add programmatically.

The intern pool conserves string storage. If you assign a literal string
constant to several variables, each variable is set to reference the same
constant in the intern pool instead of referencing several different
instances of String that have identical values.

This method looks up string in the intern pool. If string has already
been interned, a reference to that instance is returned; otherwise, a
null reference is returned.

[string reference]

opcode: 0x28 + token --> 5 bytes

total: 25 bytes
IL_0019: stloc.0
store the reference to the local variable (for later reuse). dup can't be
used because execution stack has to be also valid if the could would
branch!

[]

opcode: 0x0A --> 1byte

total: 26 bytes
IL_001a: ldloc.0
load it back for comparing

[string reference]

opcode: 0x06 --> 1 byte

total: 27 bytes
IL_001b: ldstr "A"
load value for compare

[string reference][A]

opcode: 0x72 + token --> 5 bytes

total: 32 bytes
IL_0020: beq.s IL_002a
branch if equal short version
will branch to ret when the suitable string was found.

[]

opcode: 0x2E + offset -> 2 bytes

total: 34 bytes
IL_0022: ldloc.0
load it back for comparing

[string reference]

opcode: 0x06 --> 1 byte

total: 35 bytes
IL_0023: ldstr "B"
load value for compare

[string reference]

opcode: 0x72 + token --> 5 bytes

total: 40 bytes
IL_0028: beq.s IL_002a
branch if equal short version
will branch to ret when the suitable string was found.

[]

opcode: 0x2E + offset -> 2 bytes

total: 42 bytes

IL_002a: ret
will return from the method passing the last item on stack (if there is
one) as return value. stack is empty -> no return value -> ok, it's a
void method

opcode: 0x2A --> 1 byte

TOTAL: 43 bytes


Hope this helps.

And yes I'm a geek and I'm even coding my COM objects in raw x86
assembler!
(Have you ever seen a fully working and valid DirectX Startupcode in just
2.272 bytes executable size?) ;-)

Greets
Peter

--
------ooo---OOO---ooo------

Peter Koen - www.kema.at
MCAD CAI/RS CASE/RS IAT

------ooo---OOO---ooo------
 
J

Jon Skeet [C# MVP]

QUESTION: What is the purpose of the two ldstr for the case statement
values? The LEAVE instruction clears out the eval stack so they aren't used
elsewhere. I thought at first that it might have something to do with the
Interned string cache, but these are constants and so should already be in
the cache.

Any ideas?

They're used for testing reference equality. The first two (at the
start of the method) make sure that they're both interned (so that
IsInterned will do the right thing) and the last two are just testing
for reference equality.

It means that switch/case with strings never has to do a full .Equals
call on every case - it only has to find out whether the string value
has been interned, and if so compare the interned version of the
string's reference to the cases listed in the code.
 
P

Peter Koen

They're used for testing reference equality. The first two (at the
start of the method) make sure that they're both interned (so that
IsInterned will do the right thing) and the last two are just testing
for reference equality.

It means that switch/case with strings never has to do a full .Equals
call on every case - it only has to find out whether the string value
has been interned, and if so compare the interned version of the
string's reference to the cases listed in the code.

Jon,

where did you get this information from? I don't think that this is
really correct.

You are talking about 2 times executing that 3 lines. but they are called
just once. didn't you notice that all other branches are heading for the
ret statment?

And how can a statement that does just a typesafe load and a leave of a
guarded block perform an equal? Can't be... the compiler would have to
"know" these structure and make asumptions about algorithms the user
might have had in mind... Can't be!

And there is no need to make sure that they're both interned. They will
be interned for sure at this place.

Sorry Jon, but in my humble opinion you are completly wrong here. If you
are sure that you are right then plz. show me where you got your infos
about this...

greets
Peter
--
------ooo---OOO---ooo------

Peter Koen - www.kema.at
MCAD CAI/RS CASE/RS IAT

------ooo---OOO---ooo------
 
J

Jon Skeet [C# MVP]

where did you get this information from? I don't think that this is
really correct.

You are talking about 2 times executing that 3 lines.

No, I was talking about executing the ldstrs at the start of the method
once, and then the ldstrs later once each with the comparisons once
each (until it gets to a match, of course).
but they are called
just once. didn't you notice that all other branches are heading for the
ret statment?
Absolutely.

And how can a statement that does just a typesafe load and a leave of a
guarded block perform an equal?

I didn't say it did. The leave.s here is really just a quick way of
clearing the stack. The first three lines of IL here just make sure
that "A" and "B" are interned, and then leave the stack clear.
And there is no need to make sure that they're both interned. They will
be interned for sure at this place.

I believe they're only interned for sure during ldstr. I think the CLR
can delay interning until a ldstr has been executed if it wishes to,
but doesn't have to.

For instance:
using System;
using System.Runtime.CompilerServices;

public class Test
{
static void Main()
{
string x = "h";
x += "ello";
Console.WriteLine (String.IsInterned(x)!=null);
InternHello();
Console.WriteLine (String.IsInterned(x)!=null);
}

[MethodImpl(MethodImplOptions.NoInlining)]
static void InternHello()
{
string y = "hello";
}
}

The output of this on my box is
False
True

If I take out the no-inlining attribute, it's
True
True
Sorry Jon, but in my humble opinion you are completly wrong here. If you
are sure that you are right then plz. show me where you got your infos
about this...

I'm pretty sure I'm right on this, although I can't remember exactly
where I first heard about it for.

In my view, here's what the code is doing in pseudo-code:

Make sure "A" is interned.

Make sure "B" is interned.

Check whether there's an interned version of args[0]: if not, it can't
match any case and we're done. Otherwise, remember the interned
reference. (Note that if args[0] wasn't interned before, it still
isn't.)

Is the interned reference the same as the "A" interned literal? If so,
we're done.

Is the interned reference the same as the "B" interned literal? If so,
we're done.


Are you disputing any particular part of that?


The only time when non-reference equality is needed is within the
IsInterned method which presumably looks it up in an optimised
hashtable, using String.Equals to check for equality.
 
B

Brian Tyler

Jon,

I tried to reproduce your results but it took a bit of effort. First I
realized I needed to compile in Debug because the Release build optimized
the ldstr "hello" right out of InternHello()...oops. But then the debug
build tells the JIT not to optimize so no inlining...damn. Finally figured
out to turn off optimization in the release build and got it working.

Tricky little devil...


Jon Skeet said:
where did you get this information from? I don't think that this is
really correct.

You are talking about 2 times executing that 3 lines.

No, I was talking about executing the ldstrs at the start of the method
once, and then the ldstrs later once each with the comparisons once
each (until it gets to a match, of course).
but they are called
just once. didn't you notice that all other branches are heading for the
ret statment?
Absolutely.

And how can a statement that does just a typesafe load and a leave of a
guarded block perform an equal?

I didn't say it did. The leave.s here is really just a quick way of
clearing the stack. The first three lines of IL here just make sure
that "A" and "B" are interned, and then leave the stack clear.
And there is no need to make sure that they're both interned. They will
be interned for sure at this place.

I believe they're only interned for sure during ldstr. I think the CLR
can delay interning until a ldstr has been executed if it wishes to,
but doesn't have to.

For instance:
using System;
using System.Runtime.CompilerServices;

public class Test
{
static void Main()
{
string x = "h";
x += "ello";
Console.WriteLine (String.IsInterned(x)!=null);
InternHello();
Console.WriteLine (String.IsInterned(x)!=null);
}

[MethodImpl(MethodImplOptions.NoInlining)]
static void InternHello()
{
string y = "hello";
}
}

The output of this on my box is
False
True

If I take out the no-inlining attribute, it's
True
True
Sorry Jon, but in my humble opinion you are completly wrong here. If you
are sure that you are right then plz. show me where you got your infos
about this...

I'm pretty sure I'm right on this, although I can't remember exactly
where I first heard about it for.

In my view, here's what the code is doing in pseudo-code:

Make sure "A" is interned.

Make sure "B" is interned.

Check whether there's an interned version of args[0]: if not, it can't
match any case and we're done. Otherwise, remember the interned
reference. (Note that if args[0] wasn't interned before, it still
isn't.)

Is the interned reference the same as the "A" interned literal? If so,
we're done.

Is the interned reference the same as the "B" interned literal? If so,
we're done.


Are you disputing any particular part of that?


The only time when non-reference equality is needed is within the
IsInterned method which presumably looks it up in an optimised
hashtable, using String.Equals to check for equality.
 
P

Peter Koen

Are you disputing any particular part of that?

No nothing.
Sorry, shouldn't post to newsgroups after having a few beers :)

Yes you're right. I was so focused on the fact that leave.s is leaving
guarded blocks that I assumed this was used for checking out the right type
of the values. But i forgot that this is already checked by csc when
producing the IL Asm.

Well...

Shit happens, I hope you aren't too angry about my post :)

greets
Peter

--
------ooo---OOO---ooo------

Peter Koen - www.kema.at
MCAD CAI/RS CASE/RS IAT

------ooo---OOO---ooo------
 
J

John Young

Are there any good tutorials or book on learning about MSIL?

Thanks

John Young


"Peter Koen" <koen-newsreply&snusnu.at> wrote in message
I have seen a very strange piece of code being generated by the C#
compiler and was hoping someone might be able to shed some light on
it. I've reduced the example down to a bare minimum and I am using the
.NET 1.1 framework. Here is the C# code:

using System;

namespace Example
{
class SwitchTest
{
static void Main(string[] args)
{
switch(args[0])
{
case "A":
break;

case "B":
break;
}
}
}
}

And this is the IL generated (pretty much the same for debug and
release):

.method private hidebysig static void Main(string[] args) cil managed
{
.entrypoint
// Code size 43 (0x2b)
.maxstack 2
.locals init (string V_0)
IL_0000: ldstr "A"
IL_0005: ldstr "B"
IL_000a: leave.s IL_000c
IL_000c: ldarg.0
IL_000d: ldc.i4.0
IL_000e: ldelem.ref
IL_000f: dup
IL_0010: stloc.0
IL_0011: brfalse.s IL_002a
IL_0013: ldloc.0
IL_0014: call string
[mscorlib]System.String::IsInterned(string) IL_0019: stloc.0
IL_001a: ldloc.0
IL_001b: ldstr "A"
IL_0020: beq.s IL_002a
IL_0022: ldloc.0
IL_0023: ldstr "B"
IL_0028: beq.s IL_002a
IL_002a: ret
} // end of method SwitchTest::Main


QUESTION: What is the purpose of the two ldstr for the case statement
values? The LEAVE instruction clears out the eval stack so they aren't
used elsewhere. I thought at first that it might have something to do
with the Interned string cache, but these are constants and so should
already be in the cache.

Any ideas?

Brian
(e-mail address removed)

Hello Brian,


I'll try to explain this line by line (mainly not for you, since you seem
to know IL, but for the other readers of this newsgroup):
IL_0000: ldstr "A"
IL_0005: ldstr "B"

A and B are now loaded on the execution stack
[A] <--- stack, first in is left.

opcodes:
0x72 + token --> 1 byte + token: 4 byte index into the string pool
0x72 + token
"token is a token of a user-defined string, whose RID portion is actually
an offset in the #US blob stream

This is used to see if all values that will be tested on the switch
statement comply with the switch type. if one of them wouldn't be a valid
unicode string the ldstr would fail and an exception would be thrown.
this is far more effective than calling some code to check the type. this
exception will be handeld internaly in the framework as a first-chance
exception and will not be propagated to the user.

total: 10 bytes
IL_000a: leave.s IL_000c
clear the stack and branch x bytes, in this case a lable is used for
calculating the branch step

The leave instruction, or its short-parameter form, is used to exit a
guarded block (a try block) or an exception handler block. You cannot use
this instruction, however, to exit a filter, finally, or fault block.
So this leave.s will (if there has not been a first chance exception that
imposed a fault block) step out of the try. now we are sure that there
are only valid types for the case labels.

[]

opcode: 0xDD + 1 byte offset --> 2 bytes

total: 12 bytes
IL_000c: ldarg.0
load first argument, that's the array reference

[array reference]

opcode: 0x02 --> 1 byte

total: 13 bytes
IL_000d: ldc.i4.0
load constant 0 as int32

[array reference][0]

opcode: 0x16 --> 1 byte

total: 14 bytes
IL_000e: ldelem.ref
load the indexed item from the referenced array

[item]

opcode: 0x9A --> 1 byte

total: 15bytes

IL_000f: dup
duplicates the item on the stack

[item][item]

opcode: 0x25 --> 1byte

total: 16bytes
IL_0010: stloc.0
stores topmost item into first local var

[item]

opcode: 0x0A --> 1byte

total: 17 bytes

IL_0011: brfalse.s IL_002a
branche if item on stack (reference to a string ) is zero.

therefore: if the item wasn't a string it branches to the part of the
code where the strings get loaded

[]

opcode: 0x2C + 1 byte --> 2byte

total: 19bytes
IL_0013: ldloc.0
load the reference back from the local if there was a valid reference
(you might think this is redundant, but it's necessary: just think about
the execution stack: there would be 1 more item after the branch if this
one hadn't been removed before to the local variable)

[item]

opcode: 0x06 --> 1 byte

total: 20 bytes
IL_0014: call string
[mscorlib]System.String::IsInterned(string)

call on Method IsInterned.
The common language runtime automatically maintains a table, called the
"intern pool", which contains a single instance of each unique literal
string constant declared in a program, as well as any unique instance of
String you add programmatically.

The intern pool conserves string storage. If you assign a literal string
constant to several variables, each variable is set to reference the same
constant in the intern pool instead of referencing several different
instances of String that have identical values.

This method looks up string in the intern pool. If string has already
been interned, a reference to that instance is returned; otherwise, a
null reference is returned.

[string reference]

opcode: 0x28 + token --> 5 bytes

total: 25 bytes
IL_0019: stloc.0
store the reference to the local variable (for later reuse). dup can't be
used because execution stack has to be also valid if the could would
branch!

[]

opcode: 0x0A --> 1byte

total: 26 bytes
IL_001a: ldloc.0
load it back for comparing

[string reference]

opcode: 0x06 --> 1 byte

total: 27 bytes
IL_001b: ldstr "A"
load value for compare

[string reference][A]

opcode: 0x72 + token --> 5 bytes

total: 32 bytes
IL_0020: beq.s IL_002a
branch if equal short version
will branch to ret when the suitable string was found.

[]

opcode: 0x2E + offset -> 2 bytes

total: 34 bytes
IL_0022: ldloc.0
load it back for comparing

[string reference]

opcode: 0x06 --> 1 byte

total: 35 bytes
IL_0023: ldstr "B"
load value for compare

[string reference]

opcode: 0x72 + token --> 5 bytes

total: 40 bytes
IL_0028: beq.s IL_002a
branch if equal short version
will branch to ret when the suitable string was found.

[]

opcode: 0x2E + offset -> 2 bytes

total: 42 bytes

IL_002a: ret
will return from the method passing the last item on stack (if there is
one) as return value. stack is empty -> no return value -> ok, it's a
void method

opcode: 0x2A --> 1 byte

TOTAL: 43 bytes


Hope this helps.

And yes I'm a geek and I'm even coding my COM objects in raw x86
assembler!
(Have you ever seen a fully working and valid DirectX Startupcode in just
2.272 bytes executable size?) ;-)

Greets
Peter

--
------ooo---OOO---ooo------

Peter Koen - www.kema.at
MCAD CAI/RS CASE/RS IAT

------ooo---OOO---ooo------
 
G

Gabriele G. Ponti

Hi,

You can start with CLI specifications that you can find under "C:\Program
Files\Microsoft Visual Studio .NET 2003\SDK\v1.1\Tool Developers Guide\docs"
(if you have the .NET Framework SDK installed [see
http://www.microsoft.com/downloads/...A6-3647-4070-9F41-A333C6B9181D&displaylang=en]).

For the books you can start with Serge Lidin's "Inside Microsoft .NET IL
Assembler" (http://www.microsoft.com/MSPress/books/5771.asp).

If you are interested in writing your own compiler I would recommend John
Gough's "Compiling for the .NET Common Language Runtime (CLR)"
(http://www.amazon.com/gp/reader/0130622966/ref=sib_dp_pt/104-2335927-007515
6#reader-link).

Regards,

Gabriele
 

Ask a Question

Want to reply to this thread or ask your own question?

You'll need to choose a username for the site, which only take a couple of moments. After that, you can post your question and our members will help you out.

Ask a Question

Top