it-swarm.com.ru

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

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

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

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

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

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

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

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

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

299
Rabarberski

Что лучше, а или б ?

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

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

244
Daniel Earwicker

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

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

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

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

207
Mark

Это зависит от языка и точного использования. Например, в 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.

66
Jon Skeet

Вот что я написал и скомпилировал в .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], а не StringBuilder. Существует очень большая разница в производительности между ними. Есть много таких случаев, когда компилятор не может оптимизировать ваш код, потому что он не может понять, что задумано в большем объеме. Но это может в значительной степени оптимизировать основные вещи для вас.

35
particle

Это гоча в 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...
24
Michael Haren

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

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

против 

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

Я скомпилировал эти коды с помощью gcc - 5.2.0. А затем я разобрал main () Этих двух кодов, и вот результат:

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 

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

15
UserX

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

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

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

11
Triptych

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

11
annakata

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

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

6
Powerlord

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

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

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

5
Marcelo Faísca

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

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

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

5
Mark Sowul

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

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

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

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

5
PhiLho

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

5
Stew S

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

5
Chris

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

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

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

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

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

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

4
R. Carr

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

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

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

3
fat

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

3
SquidScareMe

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

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 секунды.

1
Alex

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

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

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

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

0
Joshua Siktar

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

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

0
SteveOC 64

У меня был тот же самый вопрос в течение долгого времени. Поэтому я протестировал еще более простой кусок кода. 

Вывод: Для таких случаев существует НЕТ разницы в производительности.

Случай с внешним контуром

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

Корпус внутреннего цикла

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

Я проверил скомпилированный файл на декомпиляторе IntelliJ и в обоих случаях я получил sameTest.class

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

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

Случай с внешним контуром

Code:
  stack=2, locals=3, args_size=1
     0: iconst_0
     1: istore_2
     2: iload_2
     3: sipush        1000
     6: if_icmpge     26
     9: iload_2
    10: iconst_2
    11: iadd
    12: istore_1
    13: getstatic     #2                  // Field Java/lang/System.out:Ljava/io/PrintStream;
    16: iload_1
    17: invokevirtual #3                  // Method Java/io/PrintStream.println:(I)V
    20: iinc          2, 1
    23: goto          2
    26: return
LocalVariableTable:
        Start  Length  Slot  Name   Signature
           13      13     1 intermediateResult   I
            2      24     2     i   I
            0      27     0  args   [Ljava/lang/String;

Корпус внутреннего цикла

Code:
      stack=2, locals=3, args_size=1
         0: iconst_0
         1: istore_1
         2: iload_1
         3: sipush        1000
         6: if_icmpge     26
         9: iload_1
        10: iconst_2
        11: iadd
        12: istore_2
        13: getstatic     #2                  // Field Java/lang/System.out:Ljava/io/PrintStream;
        16: iload_2
        17: invokevirtual #3                  // Method Java/io/PrintStream.println:(I)V
        20: iinc          1, 1
        23: goto          2
        26: return
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
           13       7     2 intermediateResult   I
            2      24     1     i   I
            0      27     0  args   [Ljava/lang/String;

Если вы обратите пристальное внимание, то только Slot, назначенный i и intermediateResult в LocalVariableTable, будет заменен как результат их порядка появления. Такая же разница в слоте отражена в других строках кода.

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

БОНУС 

Компиляторы делают тонну оптимизации, взгляните на то, что происходит в этом случае.

Нулевой кейс

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

Ноль работы декомпилированы

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

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

double intermediateResult;
int i = byte.MinValue;

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

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

0
luka

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

Вы наверняка столкнетесь с проблемами утечки памяти! Поэтому я считаю, что «А» безопаснее, а «В» уязвим для накопления памяти, особенно при работе с библиотеками с закрытыми исходными кодами. Вы можете проверить использование инструмента «Valgrind» в Linux, в частности, вспомогательного инструмента «Helgrind». 

0
enthusiasticgeek

Я тестировал на JS с Node 4.0.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)
0
user137717