Разница между объявлением переменных до или в цикле?

Я всегда задавался вопросом, если, в общем, объявление выбрасываемой переменной перед циклом, в отличие от многократного внутри цикла, имеет какое-либо (производительность) значение? А (совершенно бессмысленно) пример в Java:

a) объявление перед циклом:

double intermediateResult;
for(int i=0; i < 1000; i++){
    intermediateResult = i;
    System.out.println(intermediateResult);
}

b) объявление (неоднократно) внутри цикла:

for(int i=0; i < 1000; i++){
    double intermediateResult = i;
    System.out.println(intermediateResult);
}

какой из них лучше,a или b?

Я подозреваю, что повторное объявление переменной (пример b) создает больше накладных расходов в теории, но это компиляторы достаточно умны, так что это не имеет значения. Пример b имеет преимущество быть более компактным и ограничивать объем переменной К где он использован. Все-таки, я склоняюсь к Код по примеру a.

Edit: меня особенно интересует случай Java.

24 ответов


Что лучше, a или b?

с точки зрения производительности, вы должны измерить его. (И на мой взгляд, если вы можете измерить разницу, компилятор не очень хорошо).

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


Ну, я запустил ваши примеры A и B 20 раз каждый, зацикливаясь 100 миллионов раз.(JVM-1.5.0)

A: среднее время выполнения: .074 сек

B: среднее время выполнения : .067 секунды

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


Это зависит от языка и точного использования. Например, в C# 1 это не имело никакого значения. В C# 2, если локальная переменная захвачена анонимным методом (или лямбда-выражением В C# 3), это может иметь очень существенное значение.

пример:

using System;
using System.Collections.Generic;

class Test
{
    static void Main()
    {
        List<Action> actions = new List<Action>();

        int outer;
        for (int i=0; i < 10; i++)
        {
            outer = i;
            int inner = i;
            actions.Add(() => Console.WriteLine("Inner={0}, Outer={1}", inner, outer));
        }

        foreach (Action action in actions)
        {
            action();
        }
    }
}

выход:

Inner=0, Outer=9
Inner=1, Outer=9
Inner=2, Outer=9
Inner=3, Outer=9
Inner=4, Outer=9
Inner=5, Outer=9
Inner=6, Outer=9
Inner=7, Outer=9
Inner=8, Outer=9
Inner=9, Outer=9

разница в том, что все действия захватывают то же самое outer переменная, но у каждого своя отдельная inner переменной.


следующее, что я написал и скомпилировал.Сеть.

double r0;
for (int i = 0; i < 1000; i++) {
    r0 = i*i;
    Console.WriteLine(r0);
}

for (int j = 0; j < 1000; j++) {
    double r1 = j*j;
    Console.WriteLine(r1);
}

это то, что я получаю от .NET рефлектор, когда CIL отображается обратно в код.

for (int i = 0; i < 0x3e8; i++)
{
    double r0 = i * i;
    Console.WriteLine(r0);
}
for (int j = 0; j < 0x3e8; j++)
{
    double r1 = j * j;
    Console.WriteLine(r1);
}

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


это gotcha в VB.NET - ... Результат Visual Basic не будет повторно инициализировать переменную в этом примере:

For i as Integer = 1 to 100
    Dim j as Integer
    Console.WriteLine(j)
    j = i
Next

' Output: 0 1 2 3 4...

это будет печатать 0 в первый раз (переменные Visual Basic имеют значения по умолчанию при объявлении!), но i каждый раз после этого.

если добавить = 0, однако, вы получаете то, что могли бы ожидать:

For i as Integer = 1 to 100
    Dim j as Integer = 0
    Console.WriteLine(j)
    j = i
Next

'Output: 0 0 0 0 0...

Я сделал простой тест:

int b;
for (int i = 0; i < 10; i++) {
    b = i;
}

vs

for (int i = 0; i < 10; i++) {
    int b = i;
}

я скомпилировал эти коды с помощью GCC - 5.2.0. А потом я разобрал главную () из этих двух кодов и вот результат:

1º:

   0x00000000004004b6 <+0>:     push   rbp
   0x00000000004004b7 <+1>:     mov    rbp,rsp
   0x00000000004004ba <+4>:     mov    DWORD PTR [rbp-0x4],0x0
   0x00000000004004c1 <+11>:    jmp    0x4004cd <main+23>
   0x00000000004004c3 <+13>:    mov    eax,DWORD PTR [rbp-0x4]
   0x00000000004004c6 <+16>:    mov    DWORD PTR [rbp-0x8],eax
   0x00000000004004c9 <+19>:    add    DWORD PTR [rbp-0x4],0x1
   0x00000000004004cd <+23>:    cmp    DWORD PTR [rbp-0x4],0x9
   0x00000000004004d1 <+27>:    jle    0x4004c3 <main+13>
   0x00000000004004d3 <+29>:    mov    eax,0x0
   0x00000000004004d8 <+34>:    pop    rbp
   0x00000000004004d9 <+35>:    ret

vs

   0x00000000004004b6 <+0>: push   rbp
   0x00000000004004b7 <+1>: mov    rbp,rsp
   0x00000000004004ba <+4>: mov    DWORD PTR [rbp-0x4],0x0
   0x00000000004004c1 <+11>:    jmp    0x4004cd <main+23>
   0x00000000004004c3 <+13>:    mov    eax,DWORD PTR [rbp-0x4]
   0x00000000004004c6 <+16>:    mov    DWORD PTR [rbp-0x8],eax
   0x00000000004004c9 <+19>:    add    DWORD PTR [rbp-0x4],0x1
   0x00000000004004cd <+23>:    cmp    DWORD PTR [rbp-0x4],0x9
   0x00000000004004d1 <+27>:    jle    0x4004c3 <main+13>
   0x00000000004004d3 <+29>:    mov    eax,0x0
   0x00000000004004d8 <+34>:    pop    rbp
   0x00000000004004d9 <+35>:    ret 

которые exaclty такой же результат asm. разве это не доказательство того, что два кода производят одно и то же?


Я всегда буду использовать (а не полагаться на компилятор), а также могу переписать:

for(int i=0, double intermediateResult=0; i<1000; i++){
    intermediateResult = i;
    System.out.println(intermediateResult);
}

Это все еще ограничивает intermediateResult в область цикла, но не объявляется повторно во время каждой итерации.


Это зависит от языка-IIRC C# оптимизирует это, поэтому нет никакой разницы, но JavaScript (например) будет делать все выделение памяти каждый раз.


на мой взгляд, b-лучшая структура. В a последнее значение intermediateResult остается после завершения цикла.

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


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


Как правило, я объявляю свои переменные во внутренней максимально возможной области. Итак, если вы не используете intermediateResult вне цикла, то я бы пошел с B.


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

Я предпочитаю второй (и попытаться убедить моего коллегу! ;-)), прочитав, что:

  • это уменьшает область переменных туда, где они необходимы, что хорошо.
  • Java оптимизирует достаточно, чтобы не сделать никакой существенной разницы в производительности. IIRC, возможно, вторая форма еще быстрее.

в любом случае, она падает в категория преждевременной оптимизации, которая зависит от качества компилятора и / или JVM.


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

учитывая, что они в основном одинаковы: обратите внимание, что версия b делает гораздо более очевидным для читателей, что переменная не используется и не может использоваться после цикла. Кроме того, вариант Б гораздо легче отрефакторить. Более трудно извлечь тело петли в свое собственный метод в версии a. кроме того, версия б уверяет вас что никакой побочный эффект к такой рефакторинг.

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


Ну, вы всегда можете сделать область для этого:

{ //Or if(true) if the language doesn't support making scopes like this
    double intermediateResult;
    for (int i=0; i<1000; i++) {
        intermediateResult = i;
        System.out.println(intermediateResult);
    }
}

таким образом, вы объявляете переменную только один раз, и она умрет, когда вы покинете цикл.


Я всегда думал, что если вы объявляете свои переменные внутри своего цикла, то вы тратите память. Если у вас есть что-то вроде этого:

for(;;) {
  Object o = new Object();
}

тогда не только объект должен быть создан для каждой итерации, но и должна быть новая ссылка, выделенная для каждого объекта. Кажется, что если сборщик мусора медленный, то у вас будет куча висячих ссылок, которые нужно очистить.

однако, если у вас есть это:

Object o;
for(;;) {
  o = new Object();
}

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


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


моя практика следующая:

  • Если тип переменной-это просто (int, double,...) Я предпочитаю вариант b (внутри).
    причина: сокращение объема переменной.

  • Если тип переменной не просто (какой-то class или struct) Я предпочитаю вариант a (снаружи).
    причина: уменьшение количества ctor-dtor звонки.


С точки зрения производительности, снаружи (намного) лучше.

public static void outside() {
    double intermediateResult;
    for(int i=0; i < Integer.MAX_VALUE; i++){
        intermediateResult = i;
    }
}

public static void inside() {
    for(int i=0; i < Integer.MAX_VALUE; i++){
        double intermediateResult = i;
    }
}

Я выполнил обе функции 1 миллиарда раз. outside() занял 65 миллисекунд. inside() занял 1,5 секунды.


A) является безопасной ставкой, чем B).........Представьте, что вы инициализируете структуру в цикле, а не " int " или "float", тогда что?

как

typedef struct loop_example{

JXTZ hi; // where JXTZ could be another type...say closed source lib 
         // you include in Makefile

}loop_example_struct;

//then....

int j = 0; // declare here or face c99 error if in loop - depends on compiler setting

for ( ;j++; )
{
   loop_example loop_object; // guess the result in memory heap?
}

вы наверняка столкнетесь с проблемами с утечками памяти!. Поэтому я считаю, что " A "безопаснее, а" B " уязвим для накопления памяти esp, работающего с библиотеками близких источников.Вы можете проверить Usinng "Valgrind" инструмент на Linux специально sub инструмент "Helgrind".


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

есть ли причина, по которой переменная должна быть глобальной?

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

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


Я тестировал для JS с узлом 4.0.0, если кто-то заинтересован. Объявление вне цикла привело к ~.Улучшение производительности 5 ms в среднем более 1000 испытаний со 100 миллионами итераций цикла на одно испытание. Поэтому я скажу, что продолжайте и напишите его самым читаемым / поддерживаемым способом, который является B, imo. Я бы поместил свой код в скрипку, но я использовал модуль узла performance-now. Вот код:

var now = require("../node_modules/performance-now")

// declare vars inside loop
function varInside(){
    for(var i = 0; i < 100000000; i++){
        var temp = i;
        var temp2 = i + 1;
        var temp3 = i + 2;
    }
}

// declare vars outside loop
function varOutside(){
    var temp;
    var temp2;
    var temp3;
    for(var i = 0; i < 100000000; i++){
        temp = i
        temp2 = i + 1
        temp3 = i + 2
    }
}

// for computing average execution times
var insideAvg = 0;
var outsideAvg = 0;

// run varInside a million times and average execution times
for(var i = 0; i < 1000; i++){
    var start = now()
    varInside()
    var end = now()
    insideAvg = (insideAvg + (end-start)) / 2
}

// run varOutside a million times and average execution times
for(var i = 0; i < 1000; i++){
    var start = now()
    varOutside()
    var end = now()
    outsideAvg = (outsideAvg + (end-start)) / 2
}

console.log('declared inside loop', insideAvg)
console.log('declared outside loop', outsideAvg)

Это лучшая форма

double intermediateResult;
int i = byte.MinValue;

for(; i < 1000; i++)
{
intermediateResult = i;
System.out.println(intermediateResult);
}

1) таким образом объявляется один раз как переменная, так и не каждая для цикла. 2) назначение это fatser thean все другие варианты. 3) таким образом, правилом bestpractice является любое объявление вне итерации for.


попробовал то же самое в Go и сравнил вывод компилятора с помощью go tool compile -S с go 1.9.4

нулевая разница, согласно выходу ассемблера.


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

вариант b) имеет смысл для меня, только если вам отчаянно нужно сделать intermediateResult недоступно после тела цикла. Но я все равно не могу представить себе такой отчаянной ситуации....

EDIT:Джон Скит сделал очень хороший момент, показывая, что объявление переменной внутри цикла может сделать фактическую семантическую разницу.