Переработка переменной ссылочного типа — создается ли новая ссылочная переменная в каждом цикле цикла, если она в нем объявлена?

#c#

Вопрос:

Заключается в следующем:

 MyObject myVariable;
for(int i = 0; i < objects.Length, i  ){
  myVariable = objects[i];
  // do stuff...
}
 

более эффективно, чем:

 for(int i = 0; i < objects.Length, i  ){
  MyObject myVariable = objects[i];
  // do stuff...
}
 

потому что новая переменная для хранения ссылки создается не каждый раз? (или разработчик достаточно умен, чтобы просто использовать одну и ту же переменную)..

(Если создается новая переменная, помещается ли она в кучу?)

Комментарии:

1. короткий ответ: нет, потому что область действия myVariable-это метод, в котором он находится, и новая переменная для хранения ссылки не создается, несмотря на то, что она объявлена в цикле for.

Ответ №1:

Нет, «переменные» существуют почти полностью ради программиста. Вы не создаете никакой дополнительной работы во время выполнения, объявляя переменную внутри метода.

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

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

 var a = b[c];
a.ToString();
// never access "a" again.
 

… было бы то же самое, что:

 b[c].ToString();
 

… потому что компилятор распознает, что ему нужно только сохранить результат b[c] достаточно долго, чтобы вызвать на нем метод, поэтому он может просто использовать регистр процессора вместо использования памяти.

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

Обновить

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

 int j;
for(int i = 0; i < 5; i  )
{
    j = i;
}
 

… и…

 for(int i = 0; i < 5; i  )
{
    int j = i;
}
 

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

 IL_0000:  ldc.i4.0    
IL_0001:  stloc.0     
IL_0002:  br.s        IL_0008
IL_0004:  ldloc.0     
IL_0005:  ldc.i4.1    
IL_0006:  add         
IL_0007:  stloc.0     
IL_0008:  ldloc.0     
IL_0009:  ldc.i4.5    
IL_000A:  blt.s       IL_0004
 

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

Комментарии:

1. Объявление внутри цикла также может помочь оптимизатору, так как теперь он знает, что (1) переменная не используется вне цикла, и (2) значение из предыдущей итерации не нужно переносить на следующую итерацию.

2. @Raymond: Оптимизатор обычно может знать все это независимо от того, где объявлена локальная переменная.

3. Правда. Это была скорее оптимизация «ленивого компилятора». (Что часто соответствует «ленивому другому разработчику, читающему ваш код».)

4. Объявление внутри цикла также может помочь ленивому другому разработчику, читающему ваш код , так как теперь он/она знает, что … 😉

5. @Pedro77: Компилятор может определить, что Math.Pi/180 это константа, поэтому он Math.Pi/180 заменит ее предварительно рассчитанным значением. Если вы разместите объявление ( double j ) в разных местах, вы не увидите разницы в коде IL. Если вы разместите назначение ( j = ... ) в разных местах, код IL будет другим. Однако JIT-компилятор, скорее всего, заметит, что j он никогда не менялся или никогда не использовался, и оптимизирует его.

Ответ №2:

Я оценил «Проблему» и согласился с Stripling Warrior. По крайней мере, это не имеет никакого значения с точки зрения скорости.

 long start = Time();
long end = Time();
Console.WriteLine("Benchmark Runtime: "   (end - start)   " Microseconds");

for(int k = 0; k < 5; k  )
{
    start = Time();
    int j;
    for (int i = 0; i < 900000000; i  )
    {
        j = i;
    }
    end = Time();
    Console.WriteLine("Benchmark 1: "   (end - start)   " Microseconds");
}

for (int k = 0; k < 5; k  )
{
    start = Time();
    for (int i = 0; i < 900000000; i  )
    {
        int j = i;
    }
    end = Time();
    Console.WriteLine("Benchmark 2: "   (end - start)   " Microseconds");
}
 

Результаты:

 Benchmark Runtime: 1 Microseconds
Benchmark 1: 1730816 Microseconds
Benchmark 1: 1725885 Microseconds
Benchmark 1: 1725629 Microseconds
Benchmark 1: 1726052 Microseconds
Benchmark 1: 1726121 Microseconds
Benchmark 2: 1725843 Microseconds
Benchmark 2: 1725576 Microseconds
Benchmark 2: 1726233 Microseconds
Benchmark 2: 1725786 Microseconds
Benchmark 2: 1729965 Microseconds
 

Ответ №3:

короткий ответ, да.

длинный ответ, да, это быстрее, но едва ли заметно, если не повторять много раз. 🙂

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

Комментарии:

1. Я не согласен с этим ответом. Это не быстрее и не предпочтительнее с точки зрения практики кодирования. Вы хотите, чтобы область действия переменных была как можно более узкой, чтобы избежать путаницы и конфликтов для других программистов.

2. У меня сложилось впечатление, что объявление переменной в цикле будет запускать инициализацию при каждом запуске цикла, тем самым замедляя процесс (хотя это не так много). и иметь его снаружи — это то же самое, но выньте часть инициализации и просто назначьте значения. Таким образом, немного быстрее.

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

4. когда я говорю «инициализировать», я не совсем имею в виду создание новой переменной, я имею в виду, что выполняется код инициализации. Я прошел курс компиляторов. Код инициализации будет выполнен, даже если он не создает новую переменную. так что технически это медленнее, просто не потому, что ему выделяется место в памяти. но тебе не из-за чего сворачивать с пути. Теперь, когда я перечитал вопрос, я думаю, что должен был подтвердить свой ответ, который в основном был бы «нет» сейчас: -)

5. Если бы вы создавали новый объект, он инициализировал бы объект. Если вы устанавливали для переменной значение (null, константа или иное), это могло бы инициализировать часть памяти, выделенную для этой переменной (при условии, что компилятор не смог ее оптимизировать). Но простое объявление переменной внутри цикла не добавляет никакого фактического кода, который будет выполняться во время компиляции. Я обновлю свой ответ более подробной информацией.