Лучше объявить переменную внутри или вне цикла? - PullRequest
31 голосов
/ 16 декабря 2011

Лучше всего делай:

variable1Type foo; 
variable2Type baa; 

foreach(var val in list) 
{
    foo = new Foo( ... ); 
    foo.x = FormatValue(val); 

    baa = new Baa(); 
    baa.main = foo; 
    baa.Do();
}

Или:

foreach(var val in list) 
{
    variable1Type foo = new Foo( ... ); 
    foo.x = FormatValue(val); 

    variable2Type baa = new Baa(); 
    baa.main = foo; 
    baa.Do();
}

Вопрос: что быстрее 1 случай или 2 случая? Разница незначительна? То же самое в реальных приложениях? Это может быть оптимизация-микро, но я действительно хочу знать, что лучше.

Ответы [ 6 ]

52 голосов
/ 16 декабря 2011

По производительности давайте попробуем конкретные примеры:

public void Method1()
{
  foreach(int i in Enumerable.Range(0, 10))
  {
    int x = i * i;
    StringBuilder sb = new StringBuilder();
    sb.Append(x);
    Console.WriteLine(sb);
  }
}
public void Method2()
{
  int x;
  StringBuilder sb;
  foreach(int i in Enumerable.Range(0, 10))
  {
    x = i * i;
    sb = new StringBuilder();
    sb.Append(x);
    Console.WriteLine(sb);
  }
}

Я сознательно выбрал и тип значения, и ссылочный тип в случае, если это влияет на вещи. Теперь IL для них:

.method public hidebysig instance void Method1() cil managed
{
    .maxstack 2
    .locals init (
        [0] int32 i,
        [1] int32 x,
        [2] class [mscorlib]System.Text.StringBuilder sb,
        [3] class [mscorlib]System.Collections.Generic.IEnumerator`1<int32> enumerator)
    L_0000: ldc.i4.0 
    L_0001: ldc.i4.s 10
    L_0003: call class [mscorlib]System.Collections.Generic.IEnumerable`1<int32> [System.Core]System.Linq.Enumerable::Range(int32, int32)
    L_0008: callvirt instance class [mscorlib]System.Collections.Generic.IEnumerator`1<!0> [mscorlib]System.Collections.Generic.IEnumerable`1<int32>::GetEnumerator()
    L_000d: stloc.3 
    L_000e: br.s L_002f
    L_0010: ldloc.3 
    L_0011: callvirt instance !0 [mscorlib]System.Collections.Generic.IEnumerator`1<int32>::get_Current()
    L_0016: stloc.0 
    L_0017: ldloc.0 
    L_0018: ldloc.0 
    L_0019: mul 
    L_001a: stloc.1 
    L_001b: newobj instance void [mscorlib]System.Text.StringBuilder::.ctor()
    L_0020: stloc.2 
    L_0021: ldloc.2 
    L_0022: ldloc.1 
    L_0023: callvirt instance class [mscorlib]System.Text.StringBuilder [mscorlib]System.Text.StringBuilder::Append(int32)
    L_0028: pop 
    L_0029: ldloc.2 
    L_002a: call void [mscorlib]System.Console::WriteLine(object)
    L_002f: ldloc.3 
    L_0030: callvirt instance bool [mscorlib]System.Collections.IEnumerator::MoveNext()
    L_0035: brtrue.s L_0010
    L_0037: leave.s L_0043
    L_0039: ldloc.3 
    L_003a: brfalse.s L_0042
    L_003c: ldloc.3 
    L_003d: callvirt instance void [mscorlib]System.IDisposable::Dispose()
    L_0042: endfinally 
    L_0043: ret 
    .try L_000e to L_0039 finally handler L_0039 to L_0043
}

.method public hidebysig instance void Method2() cil managed
{
    .maxstack 2
    .locals init (
        [0] int32 x,
        [1] class [mscorlib]System.Text.StringBuilder sb,
        [2] int32 i,
        [3] class [mscorlib]System.Collections.Generic.IEnumerator`1<int32> enumerator)
    L_0000: ldc.i4.0 
    L_0001: ldc.i4.s 10
    L_0003: call class [mscorlib]System.Collections.Generic.IEnumerable`1<int32> [System.Core]System.Linq.Enumerable::Range(int32, int32)
    L_0008: callvirt instance class [mscorlib]System.Collections.Generic.IEnumerator`1<!0> [mscorlib]System.Collections.Generic.IEnumerable`1<int32>::GetEnumerator()
    L_000d: stloc.3 
    L_000e: br.s L_002f
    L_0010: ldloc.3 
    L_0011: callvirt instance !0 [mscorlib]System.Collections.Generic.IEnumerator`1<int32>::get_Current()
    L_0016: stloc.2 
    L_0017: ldloc.2 
    L_0018: ldloc.2 
    L_0019: mul 
    L_001a: stloc.0 
    L_001b: newobj instance void [mscorlib]System.Text.StringBuilder::.ctor()
    L_0020: stloc.1 
    L_0021: ldloc.1 
    L_0022: ldloc.0 
    L_0023: callvirt instance class [mscorlib]System.Text.StringBuilder [mscorlib]System.Text.StringBuilder::Append(int32)
    L_0028: pop 
    L_0029: ldloc.1 
    L_002a: call void [mscorlib]System.Console::WriteLine(object)
    L_002f: ldloc.3 
    L_0030: callvirt instance bool [mscorlib]System.Collections.IEnumerator::MoveNext()
    L_0035: brtrue.s L_0010
    L_0037: leave.s L_0043
    L_0039: ldloc.3 
    L_003a: brfalse.s L_0042
    L_003c: ldloc.3 
    L_003d: callvirt instance void [mscorlib]System.IDisposable::Dispose()
    L_0042: endfinally 
    L_0043: ret 
    .try L_000e to L_0039 finally handler L_0039 to L_0043
}

Как вы можете видеть, помимо порядка в стеке, который случайно выбрал компилятор - который мог бы быть и другим порядком - это не имело абсолютно никакого эффекта. В свою очередь, на самом деле нет ничего, что один дает джиттеру, чтобы использовать его так, как другой не дает.

Кроме этого, есть одна разница.

В моих Method1(), x и sb относятся к foreach, и к ним нельзя получить преднамеренный или случайный доступ вне его.

В моем Method2(), x и sb не известны во время компиляции, чтобы им было надежно присвоено значение в foreach (компилятор не знает, что foreach выполнит хотя бы один цикл), поэтому его использование запрещено.

Пока никакой разницы нет.

I может однако назначить и использовать x и / или sb за пределами foreach. Как правило, я бы сказал, что в большинстве случаев это, вероятно, плохая область видимости, поэтому я предпочел бы Method1, но у меня могла бы быть веская причина хотеть ссылаться на них (более реалистично, если бы они не были назначены) в этом случае я бы пошел на Method2.

Тем не менее, все зависит от того, можно ли расширять каждый код или нет, а не от того, какой код написан. На самом деле, нет никакой разницы.

4 голосов
/ 16 декабря 2011

Это не имеет значения, это никак не влияет на производительность.

, но я действительно хочу знать, как поступить правильно.

Большинство расскажет вам о себе-петля имеет смысл.

3 голосов
/ 16 декабря 2011

Это просто вопрос масштабов.В этом случае, когда foo и baa используются только внутри цикла for, рекомендуется объявить их внутри цикла.Это тоже безопаснее.

1 голос
/ 16 декабря 2011

ОК, я ответил на это, не заметив, где оригинальный постер создавал новый объект каждый раз, проходя цикл, а не просто «используя» объект.Таким образом, нет, на самом деле не должно быть незначительной разницы, когда речь идет о производительности.С этим я пошел бы со вторым методом и объявил бы объект в цикле.Таким образом, он будет очищен во время следующего прохода GC, и объект будет поддерживаться в области видимости.

--- Я оставлю свой оригинальный ответ только потому, что я все это напечатал, и он может кому-то помочьеще кто ищет это сообщение позже.Я обещаю, что в будущем я уделю больше внимания прежде, чем попытаюсь ответить на следующий вопрос.

Тим

На самом деле, я думаю, что есть разница.Если я правильно помню, каждый раз, когда вы создаете новый объект = new foo(), вы добавляете этот объект в кучу памяти.Итак, создавая объекты в цикле, вы будете добавлять к издержкам системы.Если вы знаете, что цикл будет маленьким, это не проблема.

Итак, если вы в конечном итоге будете в цикле, скажем, с 1000 объектами, вы создадите 1000 переменных, которые не будутутилизировать до следующего сбора мусора.Теперь попадайте в базу данных, где вы хотите что-то сделать, и у вас есть более 20 000 строк для работы ... Вы можете создать вполне системную потребность в зависимости от того, какой тип объекта вы создаете.

Это должно быть легко проверить... Создайте приложение, которое создает 10000 элементов с отметкой времени при входе в цикл и при выходе.В первый раз, когда вы это сделаете, объявите переменную перед циклом и в следующий раз во время цикла.Возможно, вам придется увеличить этот счет намного выше, чем на 10 000, чтобы увидеть реальную разницу в скорости.

Также существует проблема с областью действия.Если он создан в цикле, он исчезнет после выхода из цикла, поэтому вы не сможете получить к нему доступ снова.Но это также помогает при очистке, поскольку сборщик мусора в конечном итоге избавится от него после выхода из цикла.

Tim

0 голосов
/ 16 декабря 2011

В JS распределение памяти каждый раз целое, в C # обычно такой разницы нет, но если локальная переменная захватывается анонимным методом, таким как лямбда-выражение, это будет иметь значение.

0 голосов
/ 16 декабря 2011

И то, и другое полностью верно, но не уверен, что есть «правильный» способ сделать это.

Ваш первый случай более экономичен (по крайней мере, в краткосрочной перспективе).Объявление ваших переменных внутри цикла приведет к большему перераспределению памяти;однако, с помощью сборщика мусора в .NET, поскольку эти переменные выходят из области видимости, они будут периодически очищаться, но не обязательно сразу.Разница в скорости, возможно, незначительна.

Второй случай, действительно, немного безопаснее, так как ограничение максимальной степени ваших переменных обычно является хорошей практикой.

...