ILGenerator emits a Break instruction when storing elements in an array - cil

I'm generating dynamic types using ILGenerator.Emit. I am generating a method body that will store the types of the method arguments in an array. To actually store the elements in the array I am looping through parameters of a given method and building up the necessary IL to store the elements. On the second iteration a Break instruction appears after the Stelem.ref (L_003d below) instruction. This always happens on the second iteration and I cannot figure out why. Here is the code:
ilGenerator.Emit(OpCodes.Ldc_I4, exampleMethod.GetParameters().Length);
ilGenerator.Emit(OpCodes.Newarr, typeof(Type));
ilGenerator.Emit(OpCodes.Stloc, typeArray);
for (int idx = 0; idx < exampleMethod.GetParameters().Length; idx++)
{
ilGenerator.Emit(OpCodes.Ldloc, typeArray);
ilGenerator.Emit(OpCodes.Ldc_I4, idx);
ilGenerator.Emit(OpCodes.Ldarg, idx + 1);
ilGenerator.Emit(OpCodes.Box, typeof(int));
ilGenerator.EmitCall(OpCodes.Callvirt, typeof(object).GetMethod("GetType"), null);
ilGenerator.Emit(OpCodes.Stelem_Ref, idx);//second iteration causes a break to be output in the IL
}
ilGenerator.Emit(OpCodes.Ret);
and the IL output is here
.method public virtual instance int32 Add3(int32, int32, int32) cil managed
{
.maxstack 3
.locals init (
[0] class [mscorlib]System.Type[] typeArray)
L_0000: ldc.i4 3
L_0005: newarr [mscorlib]System.Type
L_000a: stloc.0
L_000b: ldloc.0
L_000c: ldc.i4 0
L_0011: ldarg A_0
L_0015: nop
L_0016: nop
L_0017: box int32
L_001c: callvirt instance class [mscorlib]System.Type [mscorlib]System.Object::GetType()
L_0021: stelem.ref
L_0022: nop
L_0023: nop
L_0024: nop
L_0025: nop
L_0026: ldloc.0
L_0027: ldc.i4 1
L_002c: ldarg A_1
L_0030: nop
L_0031: nop
L_0032: box int32
L_0037: callvirt instance class [mscorlib]System.Type [mscorlib]System.Object::GetType()
L_003c: stelem.ref
**L_003d: break**
L_003e: nop
L_003f: nop
L_0040: nop
L_0041: ldloc.0
L_0042: ldc.i4 2
L_0047: ldarg A_2
L_004b: nop
L_004c: nop
L_004d: box int32
L_0052: callvirt instance class [mscorlib]System.Type [mscorlib]System.Object::GetType()
L_0057: stelem.ref
L_0058: ldarg.0
L_0059: nop
L_005a: nop
L_005b: nop
L_005c: ret
}
Any pointers or suggestions would be greatly appreciated.
Many thanks
Dermot

The opcode for break is 0x01, which incidentally is also the idx value you pass as a parameter to the stelem.ref emit. Note that there's an extra ldarg.0 on the third iteration (where idx is 2).
You should not specify a parameter to the stelem emit.

Related

Coalesce expression causing an issue in return statements in Fody

Consider the following snippet of code:
private string GetFieldValueAsString(string nonAliasedEntityName = null, string nonAliasedName = null)
{
return nonAliasedEntityName ?? nonAliasedName; // simplified code of course!
}
It is compiled to the following IL code:
.method private hidebysig
instance string GetFieldValueAsString (
[opt] string nonAliasedEntityName,
[opt] string nonAliasedName
) cil managed
{
.param [1] = nullref
.param [2] = nullref
// Method begins at RVA 0x2cb6f
// Code size 7 (0x7)
.maxstack 8
IL_0000: ldarg.1
IL_0001: dup
IL_0002: brtrue.s IL_0006
IL_0004: pop
IL_0005: ldarg.2
IL_0006: ret
} // end of method MessageBuilder::GetFieldValueAsString
If I apply 'fix the returns' in Fody, I get the following IL code:
.method private hidebysig
instance string GetFieldValueAsString (
[opt] string nonAliasedEntityName,
[opt] string nonAliasedName
) cil managed
{
.param [1] = nullref
.param [2] = nullref
// Method begins at RVA 0x2cb70
// Code size 15 (0xf)
.maxstack 3
.locals (
[0] string $returnVariable
)
IL_0000: ldarg.1
IL_0001: dup
IL_0002: dup
IL_0003: stloc.0
IL_0004: brtrue.s IL_000b
IL_0006: pop
IL_0007: ldarg.2
IL_0008: stloc.0
IL_0009: br.s IL_000b
IL_000b: nop
IL_000c: nop
IL_000d: ldloc.0
IL_000e: ret
} // end of method MessageBuilder::GetFieldValueAsString
It gives me the following error when decompiling in ILSpy, and fails to run:
ICSharpCode.Decompiler.DecompilerException: Error decompiling System.String LinkDev.Notifications.Steps.MessageBuilder::GetFieldValueAsString(System.String,System.String)
---> System.Exception: Inconsistent stack size at IL_09
at ICSharpCode.Decompiler.ILAst.ILAstBuilder.StackAnalysis(MethodDefinition methodDef)
at ICSharpCode.Decompiler.ILAst.ILAstBuilder.Build(MethodDefinition methodDef, Boolean optimize, DecompilerContext context)
at ICSharpCode.Decompiler.Ast.AstMethodBodyBuilder.CreateMethodBody(IEnumerable`1 parameters)
at ICSharpCode.Decompiler.Ast.AstMethodBodyBuilder.CreateMethodBody(MethodDefinition methodDef, DecompilerContext context, IEnumerable`1 parameters)
--- End of inner exception stack trace ---
at ICSharpCode.Decompiler.Ast.AstMethodBodyBuilder.CreateMethodBody(MethodDefinition methodDef, DecompilerContext context, IEnumerable`1 parameters)
at ICSharpCode.Decompiler.Ast.AstBuilder.CreateMethod(MethodDefinition methodDef)
at ICSharpCode.Decompiler.Ast.AstBuilder.AddMethod(MethodDefinition method)
at ICSharpCode.ILSpy.CSharpLanguage.DecompileMethod(MethodDefinition method, ITextOutput output, DecompilationOptions options)
at ICSharpCode.ILSpy.TextView.DecompilerTextView.DecompileNodes(DecompilationContext context, ITextOutput textOutput)
at ICSharpCode.ILSpy.TextView.DecompilerTextView.<>c__DisplayClass31_0.<DecompileAsync>b__0()
I traced the stack size, and it seems to hold. Any idea what could be causing this issue?
Update 1:
I added a temporary quick fix for the issue:
Instruction doubleDupInstruction = null;
for (var index = 0; index < instructions.Count; index++)
{
var instruction = instructions[index];
if (instruction.OpCode == OpCodes.Dup && instructions[index + 1].OpCode == OpCodes.Dup)
{
doubleDupInstruction = instructions[index + 1];
}
if (instruction.OpCode == OpCodes.Pop && doubleDupInstruction != null)
{
var extraPopInstruction = instructions[index];
ilProcessor.Remove(extraPopInstruction);
ilProcessor.InsertAfter(doubleDupInstruction, extraPopInstruction);
doubleDupInstruction = null;
}
}
So far it works in a decent sized program. I will keep monitoring it, and will update if anything changes. It would be much better if I could find the source of the issue in the 'return fixer'.
I think you have an inconsistent stack size when arriving at IL_000b depending on coming from IL0004 or IL0009.
Here is my analysis, the value on the right is the stack size after executing the respective line.
[0]
IL_0000: ldarg.1 [1]
IL_0001: dup [2]
IL_0002: dup [3]
IL_0003: stloc.0 [2]
IL_0004: brtrue.s IL_000b [1]---
|
IL_0006: pop [0] |
IL_0007: ldarg.2 [1] |
IL_0008: stloc.0 [0] |
IL_0009: br.s IL_000b [0] |
v
IL_000b: nop [?] arriving here is inconsistent
IL_000c: nop
IL_000d: ldloc.0
IL_000e: ret
Maybe you can run PEVerify against this method and see whats the output there.

Is it a waste of time to call a function in the foreach definition?

Sometimes I have to run a loop where the list or array comes from a function.
I usually do it this way :
Dim list = SomeClass.GetMyList()
For Each item in list
'Do some stuff
Next
Is it the same as :
For Each item in SomeClass.GetMyList()
'Do some stuff
Next
I usually do the first way because I think that second way makes a call everytime it starts the next iteration, therefore wasting some time.
Am I right to think that way ? Or can I go ahead with second way as the compiler is smart enough not to make a call every round ?
Only what is within the for block gets repeated, not its initialiser.
Your 2nd option does the same as the 1st, just with an unnamed temporary variable holding the result of GetMyList(). If anything, it might be more efficient for that reason... though a good optimiser would make both pieces of code equivalent anyway.
As mentioned in the comments, a debugger would've made this abundantly clear, and is an invaluable tool for countless other reasons.
Method #1 leaves you with a reference to the list in the scope of the rest of the method.
Method #2 creates a variable behind the scenes referencing the list, but that variable is out of scope after the for loop
For scoping I would prefer #2, but I am also impartial to succinct code. If GetMyList returns a reference type such as a List<T> or array, this could leave the door open to some unintended side effects.
Public Sub Foo()
Dim someClass As New SomeClass()
' this variable stays in scope after the following For Each loop
Dim list = someClass.GetMyList()
For Each item In list
Console.Write(item)
Next
Console.WriteLine()
' now we can sort the backing field - did you intend for this to happen?
list.Sort()
' the following For Each loop doesn't leave any reference behind
For Each item In someClass.GetMyList()
Console.Write(item)
Next
End Sub
Private Class SomeClass
Private _list As List(Of Integer) = {3, 2, 1}.ToList()
Public Function GetMyList() As List(Of Integer)
Return _list
End Function
End Class
Foo() writes:
321
123
So you can actually manipulate the backing field after you were supposedly done with it!
let take a simple example
there is not much information, i will assume GetMyList is a list(of integer)
Module Module1
Sub Main()
test1()
test2()
Console.ReadKey(False)
End Sub
Sub test1()
Dim list = SomeClass.GetMyList()
For Each item In list
Console.WriteLine(item)
Next
End Sub
Sub test2()
For Each item In SomeClass.GetMyList()
Console.WriteLine(item)
Next
End Sub
End Module
Class SomeClass
Public Shared Function GetMyList() As List(Of Integer)
Dim aList = New List(Of Integer)
aList.Add(1)
aList.Add(2)
aList.Add(3)
Console.WriteLine("I am in the function now")
Return aList
End Function
End Class
you can run it yourself to see the behavior
now let look at the actual IL (compiled with debug)
test1 code;
.method public static
void test1 () cil managed
{
// Method begins at RVA 0x2120
// Code size 64 (0x40)
.maxstack 1
.locals init (
[0] class [mscorlib]System.Collections.Generic.List`1<int32> list,
[1] int32 item,
[2] valuetype [mscorlib]System.Collections.Generic.List`1/Enumerator<int32> VB$t_struct$L0,
[3] bool VB$CG$t_bool$S0
)
IL_0000: nop
IL_0001: call class [mscorlib]System.Collections.Generic.List`1<int32> ConsoleApplication1.SomeClass::GetMyList()
IL_0006: stloc.0
IL_0007: nop
.try
{
IL_0008: ldloc.0
IL_0009: callvirt instance valuetype [mscorlib]System.Collections.Generic.List`1/Enumerator<!0> class [mscorlib]System.Collections.Generic.List`1<int32>::GetEnumerator()
IL_000e: stloc.2
IL_000f: br.s IL_0021
// loop start (head: IL_0021)
IL_0011: ldloca.s VB$t_struct$L0
IL_0013: call instance !0 valuetype [mscorlib]System.Collections.Generic.List`1/Enumerator<int32>::get_Current()
IL_0018: stloc.1
IL_0019: ldloc.1
IL_001a: call void [mscorlib]System.Console::WriteLine(int32)
IL_001f: nop
IL_0020: nop
IL_0021: ldloca.s VB$t_struct$L0
IL_0023: call instance bool valuetype [mscorlib]System.Collections.Generic.List`1/Enumerator<int32>::MoveNext()
IL_0028: stloc.3
IL_0029: ldloc.3
IL_002a: brtrue.s IL_0011
// end loop
IL_002c: nop
IL_002d: leave.s IL_003e
} // end .try
finally
{
IL_002f: ldloca.s VB$t_struct$L0
IL_0031: constrained. valuetype [mscorlib]System.Collections.Generic.List`1/Enumerator<int32>
IL_0037: callvirt instance void [mscorlib]System.IDisposable::Dispose()
IL_003c: nop
IL_003d: endfinally
} // end handler
IL_003e: nop
IL_003f: ret
} // end of method Module1::test1
test2 code;
.method public static
void test2 () cil managed
{
// Method begins at RVA 0x217c
// Code size 62 (0x3e)
.maxstack 1
.locals init (
[0] int32 item,
[1] valuetype [mscorlib]System.Collections.Generic.List`1/Enumerator<int32> VB$t_struct$L0,
[2] bool VB$CG$t_bool$S0
)
IL_0000: nop
IL_0001: nop
.try
{
IL_0002: call class [mscorlib]System.Collections.Generic.List`1<int32> ConsoleApplication1.SomeClass::GetMyList()
IL_0007: callvirt instance valuetype [mscorlib]System.Collections.Generic.List`1/Enumerator<!0> class [mscorlib]System.Collections.Generic.List`1<int32>::GetEnumerator()
IL_000c: stloc.1
IL_000d: br.s IL_001f
// loop start (head: IL_001f)
IL_000f: ldloca.s VB$t_struct$L0
IL_0011: call instance !0 valuetype [mscorlib]System.Collections.Generic.List`1/Enumerator<int32>::get_Current()
IL_0016: stloc.0
IL_0017: ldloc.0
IL_0018: call void [mscorlib]System.Console::WriteLine(int32)
IL_001d: nop
IL_001e: nop
IL_001f: ldloca.s VB$t_struct$L0
IL_0021: call instance bool valuetype [mscorlib]System.Collections.Generic.List`1/Enumerator<int32>::MoveNext()
IL_0026: stloc.2
IL_0027: ldloc.2
IL_0028: brtrue.s IL_000f
// end loop
IL_002a: nop
IL_002b: leave.s IL_003c
} // end .try
finally
{
IL_002d: ldloca.s VB$t_struct$L0
IL_002f: constrained. valuetype [mscorlib]System.Collections.Generic.List`1/Enumerator<int32>
IL_0035: callvirt instance void [mscorlib]System.IDisposable::Dispose()
IL_003a: nop
IL_003b: endfinally
} // end handler
IL_003c: nop
IL_003d: ret
} // end of method Module1::test2
only thing different is when the GetMyList reference is called / loaded in memory
first one load it in a local variable, the second one just load it when the loop start
so both scenario will do the same thing.

Declaration inside the loop will maintain the memory space or create multiple space?

Consider the method which i written as a sample of this question:
Public Sub Myfun()
Dim loopcount As Integer
For loopcount = 0 To 1000
Dim x as integer = 2
x * = loopcount
ListBox1.Items.Add (x)
Next
End Sub
My question is that this type of declaration will create how much of memory space for x.
how it is being identified if it creates 1000 separate spaces with the same name as x?
No, x is a local variable albeit declared inside a loop and this affects its visibility not the memory space occupied by that variable.
Looking at the IL code generated when you remove the ListBox1 object it is pretty clear what happens
IL_0000: ldc.i4.0 // store 0 on top of stack
IL_0001: stloc.0 // load first local variable (loopcount) with top of stack
IL_0002: ldc.i4.2 // store 2 on top of stack
IL_0003: stloc.1 // load second local variable (x) with top of the stack
IL_0004: ldloc.1 // store x on top of the stack
IL_0005: ldloc.0 // store loopcount on top of the stack
IL_0006: mul.ovf // multiply with overflow check the last two integers on stack
IL_0007: stloc.1 // store the top of stack in second local variable (x)
IL_0008: ldloc.0 // following is the logic of the for .... loop
IL_0009: ldc.i4.1
IL_000A: add.ovf
IL_000B: stloc.0
IL_000C: ldloc.0
IL_000D: ldc.i4 E8 03 00 00
IL_0012: ble.s IL_0002 // Repeat the loop from IL_002

F# "for loop" optimization

Code example:
let foo1 (arr : int[]) =
for i = 0 to arr.Length-1 do
arr.[i] <- i
let foo2 (arr : int[]) =
for i in [0..arr.Length-1] do
arr.[i] <- i
I thought that this functions should be equivalent to each other (in terms of performance). But if we look into IL listing, we'll see:
First function, 15 lines, no dynamic allocations, no try operator, no virtual calling:
IL_0000: nop
IL_0001: ldc.i4.0
IL_0002: stloc.0
IL_0003: br.s IL_0011
// loop start (head: IL_0011)
IL_0005: ldarg.0
IL_0006: ldloc.0
IL_0007: ldloc.0
IL_0008: stelem.any [mscorlib]System.Int32
IL_000d: ldloc.0
IL_000e: ldc.i4.1
IL_000f: add
IL_0010: stloc.0
IL_0011: ldloc.0
IL_0012: ldarg.0
IL_0013: ldlen
IL_0014: conv.i4
IL_0015: blt.s IL_0005
// end loop
IL_0017: ret
and second one - almost 100 lines, lots of allocations/deallocations, callings of virtual functions, lots of try/Dispose:
IL_0000: nop
IL_0001: ldc.i4.0
IL_0002: ldc.i4.1
IL_0003: ldarg.0
IL_0004: ldlen
IL_0005: conv.i4
IL_0006: ldc.i4.1
IL_0007: sub
IL_0008: call class [mscorlib]System.Collections.Generic.IEnumerable`1<int32> [FSharp.Core]Microsoft.FSharp.Core.Operators/OperatorIntrinsics::RangeInt32(int32, int32, int32)
IL_000d: call class [mscorlib]System.Collections.Generic.IEnumerable`1<!!0> [FSharp.Core]Microsoft.FSharp.Core.Operators::CreateSequence<int32>(class [mscorlib]System.Collections.Generic.IEnumerable`1<!!0>)
IL_0012: call class [FSharp.Core]Microsoft.FSharp.Collections.FSharpList`1<!!0> [FSharp.Core]Microsoft.FSharp.Collections.SeqModule::ToList<int32>(class [mscorlib]System.Collections.Generic.IEnumerable`1<!!0>)
IL_0017: stloc.0
IL_0018: ldloc.0
IL_0019: unbox.any class [mscorlib]System.Collections.Generic.IEnumerable`1<int32>
IL_001e: callvirt instance class [mscorlib]System.Collections.Generic.IEnumerator`1<!0> class [mscorlib]System.Collections.Generic.IEnumerable`1<int32>::GetEnumerator()
IL_0023: stloc.1
.try
{
// loop start (head: IL_0024)
IL_0024: ldloc.1
IL_0025: callvirt instance bool [mscorlib]System.Collections.IEnumerator::MoveNext()
IL_002a: brfalse.s IL_003e
IL_002c: ldloc.1
IL_002d: callvirt instance !0 class [mscorlib]System.Collections.Generic.IEnumerator`1<int32>::get_Current()
IL_0032: stloc.3
IL_0033: ldarg.0
IL_0034: ldloc.3
IL_0035: ldloc.3
IL_0036: stelem.any [mscorlib]System.Int32
IL_003b: nop
IL_003c: br.s IL_0024
// end loop
IL_003e: ldnull
IL_003f: stloc.2
IL_0040: leave.s IL_005b
} // end .try
finally
{
IL_0042: ldloc.1
IL_0043: isinst [mscorlib]System.IDisposable
IL_0048: stloc.s 4
IL_004a: ldloc.s 4
IL_004c: brfalse.s IL_0058
IL_004e: ldloc.s 4
IL_0050: callvirt instance void [mscorlib]System.IDisposable::Dispose()
IL_0055: ldnull
IL_0056: pop
IL_0057: endfinally
IL_0058: ldnull
IL_0059: pop
IL_005a: endfinally
} // end handler
IL_005b: ldloc.2
IL_005c: pop
IL_005d: ret
My question is why does F# compiler uses so complicated code for foo2? Why does it use an IEnumerable to implement so trivial loop?
In the 2nd example if you use range expression, it will be converted into normal for loop:
let foo2 (arr : int[]) =
for i in 0..arr.Length-1 do
arr.[i] <- i
and become equivalent to foo1.
I quote Section 6.3.12 Range Expressions in F# language specs:
A sequence iteration expression of the form for var in expr1 .. expr2
do expr3 done is sometimes elaborated as a simple for loop-expression
(§6.5.7).
However, your 2nd example is more like:
let foo2 (arr : int[]) =
let xs = [0..arr.Length-1] (* A new list is created *)
for i in xs do
arr.[i] <- i
where you have created a new list explicitly.
What your seeing is the standard difference between using an index based enumeration and an IEnumerable based enumeration (or in C# terms for vs foreach).
In the second sample the expression [0..arr.Length-1] is creating a collection and F# is using IEnumerable<T> to enumerate the values. Part of this enumeration style involves the use of IEnumerator<T> which implements IDisposable. The try / finally block you are seeing is generated to ensure that the IDisposable::Dispose method is called at the end of the enumeration even in the face of an exception.
Could F# optimize the second example into the first and avoid all of the extra overhead? It's possible that they could do this optimization. Essentially peek through the range expression, not it's just a simple numeric range and hence generate the equivalent for code.
Should F# optimize the second example. My vote would be no. Features like this often look trivial from the outside but actually implementing them, and more importantly maintaining them, can be rather expensive. An astute user could always convert their code back to the standard for version and avoid the IEnumerable<T> overhead (should the profiler reveal it to be an issue). Not implementing the optimization frees up the F# team to implement other awesome features.

CInt(Long) in VB.NET behaving differently in 32- and 64-bit environments

Today I had a problem converting a Long (Int64) to an Integer (Int32). The problem is that my code was always working in 32-bit environments, but when I try THE SAME executable in a 64-bit computer it crashes with a System.OverflowException exception.
I've prepared this test code in Visual Studio 2008 in a new project with default settings:
Module Module1
Sub Main()
Dim alpha As Long = -1
Dim delta As Integer
Try
delta = CInt(alpha And UInteger.MaxValue)
Console.WriteLine("CINT OK")
delta = Convert.ToInt32(alpha And UInteger.MaxValue)
Console.WriteLine("Convert.ToInt32 OK")
Catch ex As Exception
Console.WriteLine(ex.GetType().ToString())
Finally
Console.ReadLine()
End Try
End Sub
End Module
On my 32-bit setups (Windows XP SP3 32-bit and Windows 7 32-bit) it prints up to "CINT OK", but in the 64-bit computer (Windows 7 64-bit) that I've tested THE SAME executable it prints the exception name only.
Is this behavior documented? I tried to find a reference, but I failed miserably.
For reference I leave the CIL code too:
.method public static void Main() cil managed
{
.entrypoint
.custom instance void [mscorlib]System.STAThreadAttribute::.ctor() = ( 01 00 00 00 )
// Code size 88 (0x58)
.maxstack 2
.locals init ([0] int64 alpha,
[1] int32 delta,
[2] class [mscorlib]System.Exception ex)
IL_0000: nop
IL_0001: ldc.i4.m1
IL_0002: conv.i8
IL_0003: stloc.0
IL_0004: nop
.try
{
.try
{
IL_0005: ldloc.0
IL_0006: ldc.i4.m1
IL_0007: conv.u8
IL_0008: and
IL_0009: conv.ovf.i4
IL_000a: stloc.1
IL_000b: ldstr "CINT OK"
IL_0010: call void [mscorlib]System.Console::WriteLine(string)
IL_0015: nop
IL_0016: ldloc.0
IL_0017: ldc.i4.m1
IL_0018: conv.u8
IL_0019: and
IL_001a: call int32 [mscorlib]System.Convert::ToInt32(int64)
IL_001f: stloc.1
IL_0020: ldstr "Convert.ToInt32 OK"
IL_0025: call void [mscorlib]System.Console::WriteLine(string)
IL_002a: nop
IL_002b: leave.s IL_0055
} // End .try
catch [mscorlib]System.Exception
{
IL_002d: dup
IL_002e: call void [Microsoft.VisualBasic]Microsoft.VisualBasic.CompilerServices.ProjectData::SetProjectError(class [mscorlib]System.Exception)
IL_0033: stloc.2
IL_0034: nop
IL_0035: ldloc.2
IL_0036: callvirt instance class [mscorlib]System.Type [mscorlib]System.Exception::GetType()
IL_003b: callvirt instance string [mscorlib]System.Type::ToString()
IL_0040: call void [mscorlib]System.Console::WriteLine(string)
IL_0045: nop
IL_0046: call void [Microsoft.VisualBasic]Microsoft.VisualBasic.CompilerServices.ProjectData::ClearProjectError()
IL_004b: leave.s IL_0055
} // End handler
} // End .try
finally
{
IL_004d: nop
IL_004e: call string [mscorlib]System.Console::ReadLine()
IL_0053: pop
IL_0054: endfinally
} // End handler
IL_0055: nop
IL_0056: nop
IL_0057: ret
} // End of method Module1::Main
I suspect that the instruction that is behaving differently is either conv.ovf.i4 or the ldc.i4.m1/conv.u8 pair.
What is going on?
Convert.ToInt32(long) fails in both environments. It is only CInt(Long) which is behaving differently.
Unfortunately, the 64-bit version is accurate. It really is an overflow, the result of the expression is a long with the value &hffffffff. The sign bit is AND-ed off the value, it is no longer negative. The resulting value cannot be converted to an integer, the maximum integer value is &h7fffffff. You can see this by adding this code to your snippet:
Dim value As Long = alpha And UInteger.MaxValue
Console.WriteLine(value)
Output: 4294967295
The x64 jitter uses an entirely different way to check for overflows, it doesn't rely on the CPU overflow exception but explicitly compares the values to Integer.MaxValue and Integer.MinValue. The x86 jitter gets it wrong, it optimizes the code too much and ends up making an unsigned operation that doesn't trip the CPU exception.
Filing a bug report at connect.microsoft.com is probably not worth the effort, fixing this for the x86 jitter would be a drastically breaking change. You'll have to rework this logic. Not sure how, I don't see what you are trying to do.
I don't know of any real reference as such, but if you go to this page:
http://msdn.microsoft.com/en-us/library/system.int32.aspx
You can see in the sample where they use CInt they do wrap it in a OverflowException handler (try searching for CInt on that page to find it). So at least they say implicitly that CInt can throw that in certain circumstances.
If you do not want the exceptions being thrown you can change the Remove integer overflow checks setting on the Advanced Compile Options page.
Try to change build platform target from “Any CPU” to "x86".
Just to complete the documentation of this issue I made this:
Imports System.Runtime.InteropServices
Module Module1
<DllImport("KERNEL32.DLL", EntryPoint:="DebugBreak", _
SetLastError:=False, CharSet:=CharSet.Unicode, _
ExactSpelling:=True, _
CallingConvention:=CallingConvention.StdCall)> _
Public Sub DebugBreak()
End Sub
Sub Main()
Dim alpha As Long = -1
Dim delta As Integer
DebugBreak() ' To call OllyDbg
' Needed to prevent the jitter from raising the overflow exception in the second CInt without really doing the convertion first
alpha = alpha Xor Environment.TickCount
Console.WriteLine(alpha)
delta = CInt(alpha And UInteger.MaxValue)
Console.WriteLine(delta)
alpha = alpha And UInteger.MaxValue
delta = CInt(alpha)
Console.WriteLine(delta)
Console.ReadLine()
End Sub
End Module
Using OllyDbg I got this:
CPU Disasm
Address Hex dump Command Comments
00D10070 55 PUSH EBP
00D10071 8BEC MOV EBP,ESP
00D10073 57 PUSH EDI
00D10074 56 PUSH ESI
00D10075 53 PUSH EBX
00D10076 E8 A1BFC7FF CALL 0098C01C
00D1007B E8 A18C1879 CALL <JMP.&KERNEL32.GetTickCount> ; Jump to KERNEL32.GetTickCount
00D10080 99 CDQ
00D10081 F7D0 NOT EAX
00D10083 F7D2 NOT EDX
00D10085 8BF0 MOV ESI,EAX
00D10087 8BFA MOV EDI,EDX
00D10089 E8 62D25D78 CALL 792ED2F0 ; Called everytime Console is referenced here
00D1008E 57 PUSH EDI
00D1008F 56 PUSH ESI
00D10090 8BC8 MOV ECX,EAX
00D10092 8B01 MOV EAX,DWORD PTR DS:[ECX]
00D10094 FF90 C4000000 CALL DWORD PTR DS:[EAX+0C4] ; Console.WriteLine(Int64)
00D1009A 8BDE MOV EBX,ESI ; Note: EDI:ESI holds alpha variable
00D1009C 83E3 FF AND EBX,FFFFFFFF ; delta = CInt(alpha And UInteger.MaxValue)
00D1009F E8 4CD25D78 CALL 792ED2F0
00D100A4 8BC8 MOV ECX,EAX
00D100A6 8BD3 MOV EDX,EBX
00D100A8 8B01 MOV EAX,DWORD PTR DS:[ECX]
00D100AA FF90 BC000000 CALL DWORD PTR DS:[EAX+0BC] ; Console.WriteLine(Int32)
00D100B0 33FF XOR EDI,EDI ; alpha = alpha And UInteger.MaxValue
00D100B2 85F6 TEST ESI,ESI ; delta = CInt(alpha) [Begins here]
00D100B4 7C 06 JL SHORT 00D100BC
00D100B6 85FF TEST EDI,EDI
00D100B8 75 2B JNE SHORT 00D100E5
00D100BA EB 05 JMP SHORT 00D100C1
00D100BC 83FF FF CMP EDI,-1
00D100BF 75 24 JNE SHORT 00D100E5
00D100C1 8BDE MOV EBX,ESI ; delta = CInt(alpha) [Ends here]
00D100C3 E8 28D25D78 CALL 792ED2F0
00D100C8 8BC8 MOV ECX,EAX
00D100CA 8BD3 MOV EDX,EBX
00D100CC 8B01 MOV EAX,DWORD PTR DS:[ECX]
00D100CE FF90 BC000000 CALL DWORD PTR DS:[EAX+0BC] ; Console.WriteLine(Int32)
00D100D4 E8 1B1AA878 CALL 79791AF4
00D100D9 8BC8 MOV ECX,EAX
00D100DB 8B01 MOV EAX,DWORD PTR DS:[ECX]
00D100DD FF50 64 CALL DWORD PTR DS:[EAX+64]
00D100E0 5B POP EBX
00D100E1 5E POP ESI
00D100E2 5F POP EDI
00D100E3 5D POP EBP
00D100E4 C3 RETN
As you can see the second CInt sentence is much more complex than just ANDing (which it could actually be suppressed as EBX won't change and the EFLAGS are not consumed anywhere). The probable origin of this problem can be seen in Hans' answer