Reputation: 8613
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?
Upvotes: 3
Views: 3064
Reputation: 119806
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
Upvotes: 3
Reputation: 700312
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.
Upvotes: 4
Reputation: 144112
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
Upvotes: 6
Reputation: 9443
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
}
Upvotes: 11
Reputation: 11515
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.
Upvotes: 0