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

Я всегда задавался вопросом, может ли, вообще говоря, объявлять переменную выброса перед циклом, а не повторять внутри цикла, делает любую (производительность) разницу? A (совершенно бессмысленный) пример в 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.

Изменить: Меня особенно интересует случай Java.

Ответ 1

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

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

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

Ответ 2

Хорошо, что я запускал ваши примеры A и B по 20 раз, цикл 100 миллионов раз (JVM - 1.5.0)

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

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

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

Ответ 3

Это зависит от языка и точного использования. Например, в С# 1 это не имело никакого значения. В С# 2, если локальная переменная захватывается анонимным методом (или выражением лямбда в С# 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.

Ответ 4

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

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 Reflector, когда 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/байтовый код, а во время выполнения он преобразуется в машинный язык. Таким образом, в машинном языке в стек может не создаваться двойной. Это может быть только регистр, поскольку код отражает, что это временная переменная для функции WriteLine. Существуют целые правила оптимизации только для циклов. Поэтому средний парень не должен беспокоиться об этом, особенно в управляемых языках. Бывают случаи, когда вы можете оптимизировать управление кодом, например, если вам нужно конкатенировать большое количество строк, используя только string a; a+=anotherstring[i] vs, используя StringBuilder. Существует очень большая разница в производительности между ними. Есть много таких случаев, когда компилятор не может оптимизировать ваш код, потому что он не может понять, что предназначено в большей области. Но он может в значительной степени оптимизировать основные вещи для вас.

Ответ 5

Это будет в 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...

Ответ 6

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

Ответ 7

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

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

против

   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 

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

Ответ 8

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

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

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

Ответ 9

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

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

Ответ 10

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

Ответ 11

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

Ответ 12

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

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

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

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

Ответ 13

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

{ //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);
    }
}

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

Ответ 14

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

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

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

Ответ 15

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

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

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

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

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

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

Ответ 16

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

Ответ 17

Моя практика такова:

  • если тип переменной прост (int, double,...) Я предпочитаю вариант b (внутри).
    Причина: уменьшение объема переменной.

  • если тип переменной не является простым (некоторая class или struct), я предпочитаю вариант a (снаружи).
    Причина: сокращение числа вызовов ctor-dtor.

Ответ 18

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

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 миллисекунд. внутри() заняло 1,5 секунды.

Ответ 19

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

like

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

У вас обязательно возникнут проблемы с утечками памяти!. Следовательно, я считаю, что "А" более безопасно, тогда как "В" уязвим для накопления памяти, работая с близкими исходными библиотеками. Вы можете проверить "Инструмент Valgrind" на Linux специально под инструментом "Helgrind".

Ответ 20

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

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

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

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

Ответ 21

Я тестировал JS с Node 4.0.0, если кто-то заинтересован. Объявление вне цикла приводило к повышению производительности ~.5 мс в среднем более 1000 испытаний с 100 миллионами итераций цикла за испытание. Так что я собираюсь сказать, вперед и написать его наиболее читаемым/поддерживаемым способом, который является B, imo. Я бы поставил свой код в скрипке, но я использовал модуль производительности - теперь Node. Здесь код:

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)

Ответ 22

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

double intermediateResult;
int i = byte.MinValue;

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

1) таким образом объявляли один раз как переменные, так и не каждый для цикла. 2) присвоение, которое он изменяет для всех остальных. 3) Таким образом, правило bestpractice - это любое объявление за пределами итерации.

Ответ 23

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

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

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