views:

1728

answers:

5

My co-worker said that in a previous interview, he learned that foreach is faster in VB.Net than c#'s foreach. He was told that this was because both have different CLR implementation.

Coming from a C++ perspective, I'm curious on why this is and I was told that I need to read up on CLR first. Googling foreach and CLR doesn't help me understand.

Does anyone have a good explanation on why foreach is faster in VB.Net than in c#? Or was my co-worker misinformed?

+6  A: 

I'm a little suspicious of this claim. The foreach construct works the same way against both languages, in that it gets the IEnumerator from the managed object and calls MoveNext() on it. Whether the original code was written in VB.NET or c# should not matter, they both compile to the same thing.

In my test timings, the same foreach loop in VB.NET and c# were never more than ~1% apart for very long iterations.

c#:

L_0048: ldloca.s CS$5$0001
L_004a: call instance !0 [mscorlib]System.Collections.Generic.List`1/Enumerator<string>::get_Current()
L_004f: stloc.3 
L_0050: nop 
L_0051: ldloc.3 
L_0052: call void [mscorlib]System.Console::WriteLine(string)
L_0057: nop 
L_0058: nop 
L_0059: ldloca.s CS$5$0001
L_005b: call instance bool [mscorlib]System.Collections.Generic.List`1/Enumerator<string>::MoveNext()
L_0060: stloc.s CS$4$0000
L_0062: ldloc.s CS$4$0000
L_0064: brtrue.s L_0048

VB.NET:

L_0043: ldloca.s VB$t_struct$L0
L_0045: call instance !0 [mscorlib]System.Collections.Generic.List`1/Enumerator<string>::get_Current()
L_004a: stloc.s item
L_004c: ldloc.s item
L_004e: call void [mscorlib]System.Console::WriteLine(string)
L_0053: nop 
L_0054: nop 
L_0055: ldloca.s VB$t_struct$L0
L_0057: call instance bool [mscorlib]System.Collections.Generic.List`1/Enumerator<string>::MoveNext()
L_005c: stloc.s VB$CG$t_bool$S0
L_005e: ldloc.s VB$CG$t_bool$S0
L_0060: brtrue.s L_0043
Rex M
That exactly what my co-worker initial answer was, only to be told wrong by the interviewer. I'm leaning on the interviewer being wrong, but I'm not proficient in c# enough to give a valid opinion. Hence, the question.
MrValdez
@MrValdez you should totally get your co-worker to write a strongly-worded letter to the interviewer once you prove that he was wrong! ;-)
Matt Hamilton
@Matt :D great suggestion! I'll tell him that.
MrValdez
A: 

You should do an experiment. Grab the (awesome) .NET Reflector, build a simple test case in each language, and see whether the generated MSIL is the same or not.

zildjohn01
I'm not a c# programmer (I've been put to work on a c# project and I'm just using my c++ experience as a guide). But if I can find the time, I'll try an experiment.
MrValdez
Run it, post results, and I'll upvote.
Joel Coehoorn
+9  A: 

There is no significant difference at the IL level between C# and VB.Net. There are some additional Nop instructions thrown in here and there between the two versions, but nothing that actually changes what is going on.

Here is the method: (in C#)

public void TestForEach()
    {
        List<string> items = new List<string> { "one", "two", "three" };

        foreach (string item in items)
        {
            Debug.WriteLine(item);
        }
    }

And in VB.Net:

Public Sub TestForEach
    Dim items As List(Of String) = New List(Of String)()
    items.Add("one")
    items.Add("two")
    items.Add("three")
    For Each item As string In items
        Debug.WriteLine(item)
    Next
End Sub

Here is the IL for the C# version:

.method public hidebysig instance void TestForEach() cil managed
{
    .maxstack 2
    .locals init (
        [0] class [mscorlib]System.Collections.Generic.List`1<string> items,
        [1] string item,
        [2] class [mscorlib]System.Collections.Generic.List`1<string> <>g__initLocal3,
        [3] valuetype [mscorlib]System.Collections.Generic.List`1/Enumerator<string> CS$5$0000,
        [4] bool CS$4$0001)
    L_0000: nop 
    L_0001: newobj instance void [mscorlib]System.Collections.Generic.List`1<string>::.ctor()
    L_0006: stloc.2 
    L_0007: ldloc.2 
    L_0008: ldstr "one"
    L_000d: callvirt instance void [mscorlib]System.Collections.Generic.List`1<string>::Add(!0)
    L_0012: nop 
    L_0013: ldloc.2 
    L_0014: ldstr "two"
    L_0019: callvirt instance void [mscorlib]System.Collections.Generic.List`1<string>::Add(!0)
    L_001e: nop 
    L_001f: ldloc.2 
    L_0020: ldstr "three"
    L_0025: callvirt instance void [mscorlib]System.Collections.Generic.List`1<string>::Add(!0)
    L_002a: nop 
    L_002b: ldloc.2 
    L_002c: stloc.0 
    L_002d: nop 
    L_002e: ldloc.0 
    L_002f: callvirt instance valuetype [mscorlib]System.Collections.Generic.List`1/Enumerator<!0> [mscorlib]System.Collections.Generic.List`1<string>::GetEnumerator()
    L_0034: stloc.3 
    L_0035: br.s L_0048
    L_0037: ldloca.s CS$5$0000
    L_0039: call instance !0 [mscorlib]System.Collections.Generic.List`1/Enumerator<string>::get_Current()
    L_003e: stloc.1 
    L_003f: nop 
    L_0040: ldloc.1 
    L_0041: call void [System]System.Diagnostics.Debug::WriteLine(string)
    L_0046: nop 
    L_0047: nop 
    L_0048: ldloca.s CS$5$0000
    L_004a: call instance bool [mscorlib]System.Collections.Generic.List`1/Enumerator<string>::MoveNext()
    L_004f: stloc.s CS$4$0001
    L_0051: ldloc.s CS$4$0001
    L_0053: brtrue.s L_0037
    L_0055: leave.s L_0066
    L_0057: ldloca.s CS$5$0000
    L_0059: constrained [mscorlib]System.Collections.Generic.List`1/Enumerator<string>
    L_005f: callvirt instance void [mscorlib]System.IDisposable::Dispose()
    L_0064: nop 
    L_0065: endfinally 
    L_0066: nop 
    L_0067: ret 
    .try L_0035 to L_0057 finally handler L_0057 to L_0066
}

Here is the IL for the VB.Net version:

.method public instance void TestForEach() cil managed
{
    .maxstack 2
    .locals init (
        [0] class [mscorlib]System.Collections.Generic.List`1<string> items,
        [1] string item,
        [2] valuetype [mscorlib]System.Collections.Generic.List`1/Enumerator<string> VB$t_struct$L0,
        [3] bool VB$CG$t_bool$S0)
    L_0000: nop 
    L_0001: newobj instance void [mscorlib]System.Collections.Generic.List`1<string>::.ctor()
    L_0006: stloc.0 
    L_0007: ldloc.0 
    L_0008: ldstr "one"
    L_000d: callvirt instance void [mscorlib]System.Collections.Generic.List`1<string>::Add(!0)
    L_0012: nop 
    L_0013: ldloc.0 
    L_0014: ldstr "two"
    L_0019: callvirt instance void [mscorlib]System.Collections.Generic.List`1<string>::Add(!0)
    L_001e: nop 
    L_001f: ldloc.0 
    L_0020: ldstr "three"
    L_0025: callvirt instance void [mscorlib]System.Collections.Generic.List`1<string>::Add(!0)
    L_002a: nop 
    L_002b: nop 
    L_002c: ldloc.0 
    L_002d: callvirt instance valuetype [mscorlib]System.Collections.Generic.List`1/Enumerator<!0> [mscorlib]System.Collections.Generic.List`1<string>::GetEnumerator()
    L_0032: stloc.2 
    L_0033: br.s L_0045
    L_0035: ldloca.s VB$t_struct$L0
    L_0037: call instance !0 [mscorlib]System.Collections.Generic.List`1/Enumerator<string>::get_Current()
    L_003c: stloc.1 
    L_003d: ldloc.1 
    L_003e: call void [System]System.Diagnostics.Debug::WriteLine(string)
    L_0043: nop 
    L_0044: nop 
    L_0045: ldloca.s VB$t_struct$L0
    L_0047: call instance bool [mscorlib]System.Collections.Generic.List`1/Enumerator<string>::MoveNext()
    L_004c: stloc.3 
    L_004d: ldloc.3 
    L_004e: brtrue.s L_0035
    L_0050: nop 
    L_0051: leave.s L_0062
    L_0053: ldloca.s VB$t_struct$L0
    L_0055: constrained [mscorlib]System.Collections.Generic.List`1/Enumerator<string>
    L_005b: callvirt instance void [mscorlib]System.IDisposable::Dispose()
    L_0060: nop 
    L_0061: endfinally 
    L_0062: nop 
    L_0063: ret 
    .try L_002c to L_0053 finally handler L_0053 to L_0062
}
ckramer
The extra nop instructions suggests that you compiled this in debug mode?
Guffa
That would be correct...it was a quick test, and I was using Debug.WriteLine() in the for each loop after all :)
ckramer
+3  A: 

For a simple foreach looping a string array, this is the IL code produced by VB:

L_0007: ldloc.0 
L_0008: stloc.3 
L_0009: ldc.i4.0 
L_000a: stloc.2 
L_000b: br.s L_0019

L_000d: ldloc.3 
L_000e: ldloc.2 
L_000f: ldelem.ref 
L_0010: stloc.1 

...

L_0015: ldloc.2 
L_0016: ldc.i4.1 
L_0017: add.ovf 
L_0018: stloc.2 

L_0019: ldloc.2 
L_001a: ldloc.3 
L_001b: ldlen 
L_001c: conv.ovf.i4 
L_001d: blt.s L_000d

And this is the IL code produced by C#:

L_0007: ldloc.0 
L_0008: stloc.2 
L_0009: ldc.i4.0 
L_000a: stloc.3 
L_000b: br.s L_0019

L_000d: ldloc.2 
L_000e: ldloc.3 
L_000f: ldelem.ref 
L_0010: stloc.1 

...

L_0015: ldloc.3 
L_0016: ldc.i4.1 
L_0017: add 
L_0018: stloc.3 

L_0019: ldloc.3 
L_001a: ldloc.2 
L_001b: ldlen 
L_001c: conv.i4 
L_001d: blt.s L_000d

The only difference is that VB uses add.ovf and conv.ovf.i4 instead of add and conv.i4. That means that the VB code does two extra overflow checks, and might be slightly slower.

Guffa
+2  A: 

VB.NET and C# both use the same CLR. I just did a quick finger in the air benchmark using the following code:

C# version:

static void Main(string[] args)
{
    List<string> myList = new List<string>();

    for(int i = 0; i < 500000; i++)
    {
     myList.Add(i.ToString());
    }

    DateTime st = DateTime.Now;
    foreach(string s in myList)
    {
     Console.WriteLine(s);
    }
    DateTime et = DateTime.Now;

    Console.WriteLine(et - st);
    Console.ReadLine();
}

VB.NET version:

Module Module1

    Sub Main()
     Dim myList As List(Of String) = New List(Of String)

     For i = 1 To 500000
      myList.Add(i)
     Next

     Dim st, et
     st = DateTime.Now
     For Each s As String In myList
      Console.WriteLine(s)
     Next
     et = DateTime.Now

     Console.WriteLine(et - st)
     Console.ReadLine()
    End Sub

End Module

On the release build (which counts most) performing 500000 iterations the C# code is marginally faster but only by a whisker.

Debug Build:

C#     - 1m 40s 457ms
VB.NET - 1m 42s 022ms

Release Build:

C#     - 0m 56s 179ms
VB.NET - 0m 56s 327ms

HTH
Kev

Kev