Является ли foreach в VB.NET быстрее, чем в c #? - PullRequest
4 голосов
/ 12 марта 2009

Мой коллега сказал, что в предыдущем интервью он узнал, что foreach в VB.Net работает быстрее, чем foreach в C #. Ему сказали, что это потому, что оба имеют разную реализацию CLR.

Исходя из перспективы C ++, мне любопытно, почему это так, и мне сказали, что мне нужно сначала прочитать CLR. Гугл foreach и CLR не помогают мне понять.

У кого-нибудь есть хорошее объяснение, почему foreach в VB.Net работает быстрее, чем в c #? Или мой коллега был дезинформирован?

Ответы [ 5 ]

11 голосов
/ 12 марта 2009

На уровне IL нет существенной разницы между C # и VB.Net. Между этими двумя версиями есть несколько дополнительных инструкций Nop, но ничего, что действительно меняет происходящее.

Вот метод: (в C #)

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

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

А в 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

Вот IL для версии C #:

.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
}

Вот IL для версии VB.Net:

.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
}
6 голосов
/ 12 марта 2009

Я немного подозрительно отношусь к этому заявлению. Конструкция foreach одинаково работает с обоими языками, поскольку она получает IEnumerator от управляемого объекта и вызывает для него MoveNext (). Независимо от того, был ли оригинальный код написан на VB.NET или на c #, они не должны иметь значения, они оба компилируются одинаково.

В моих тестовых таймингах один и тот же цикл foreach в VB.NET и c # никогда не расходился более чем на ~ 1% для очень длинных итераций.

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
4 голосов
/ 12 марта 2009

Для простого цикла foreach, зацикливающего массив строк, это код IL, созданный 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

И это код IL, созданный 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

Единственное отличие состоит в том, что VB использует add.ovf и conv.ovf.i4 вместо add и conv.i4. Это означает, что код VB выполняет две дополнительные проверки переполнения и может быть немного медленнее.

3 голосов
/ 12 марта 2009

VB.NET и C # оба используют один и тот же CLR. Я просто быстро провел тест в воздухе, используя следующий код:

C # версия:

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 версия:

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

В сборке релиза (которая насчитывает больше всего), выполняющей 500000 итераций, код C # немного быстрее, но только на волосок.

Debug Build:

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

Выпуск сборки:

C#     - 0m 56s 179ms
VB.NET - 0m 56s 327ms
0 голосов
/ 12 марта 2009

Вы должны сделать эксперимент. Возьмите (удивительный) .NET Reflector , создайте простой тестовый пример на каждом языке и посмотрите, является ли сгенерированный MSIL одинаковым или нет.

...