Объявление переменных внутри или вне цикла


236

Почему следующее работает нормально?

String str;
while (condition) {
    str = calculateStr();
    .....
}

Но этот считается опасным / неправильным:

while (condition) {
    String str = calculateStr();
    .....
}

Нужно ли объявлять переменные вне цикла?

Ответы:


289

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

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

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

Итак, ответ решительно, что strабсолютно должно быть объявлено в цикле while. Нет, если, нет и нет, но нет.

Единственный случай, когда это правило может быть нарушено, - это если по какой-то причине жизненно важно, чтобы каждый тактовый цикл выжимался из кода, и в этом случае вы можете рассмотреть создание экземпляра чего-либо во внешней области видимости и его повторное использование вместо повторное создание его на каждой итерации внутренней области видимости. Однако это не относится к вашему примеру из-за неизменности строк в java: новый экземпляр str всегда будет создаваться в начале вашего цикла, и его нужно будет выбрасывать в конце, поэтому нет возможности оптимизировать там.

РЕДАКТИРОВАТЬ: (вставляя мой комментарий ниже в ответе)

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


2
Запрос по последнему абзацу: Если это был другой, то String, который не является неизменяемым, влияет ли это?
Гарри Джой

1
@HarryJoy Да, конечно, возьмем, например, StringBuilder, который является изменяемым. Если вы используете StringBuilder для построения новой строки в каждой итерации цикла, то вы можете оптимизировать вещи, выделяя StringBuilder вне цикла. Но все же, это не рекомендуемая практика. Если вы делаете это без веской причины, это преждевременная оптимизация.
Майк Накис

7
@HarryJoy Правильный способ сделать что - то, чтобы написать весь код правильно , установить требование к производительности для вашего продукта, измерить ваш конечный продукт против этого требования, и если оно не удовлетворяет его, затем оптимизировать вещи. И знаешь, что? Обычно вы сможете обеспечить несколько хороших и формальных алгоритмических оптимизаций всего в нескольких местах, которые сделают свое дело вместо того, чтобы обходить весь ваш код и настраивать и взламывать вещи, чтобы сжимать тактовые циклы здесь и там.
Майк Накис

2
@MikeNakis Я думаю, ты думаешь в очень узкой области.
Ситен

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

293

Я сравнил байт-код этих двух (похожих) примеров:

Давайте посмотрим на 1. пример :

package inside;

public class Test {
    public static void main(String[] args) {
        while(true){
            String str = String.valueOf(System.currentTimeMillis());
            System.out.println(str);
        }
    }
}

после того, как javac Test.java, javap -c Testвы получите:

public class inside.Test extends java.lang.Object{
public inside.Test();
  Code:
   0:   aload_0
   1:   invokespecial   #1; //Method java/lang/Object."<init>":()V
   4:   return

public static void main(java.lang.String[]);
  Code:
   0:   invokestatic    #2; //Method java/lang/System.currentTimeMillis:()J
   3:   invokestatic    #3; //Method java/lang/String.valueOf:(J)Ljava/lang/String;
   6:   astore_1
   7:   getstatic       #4; //Field java/lang/System.out:Ljava/io/PrintStream;
   10:  aload_1
   11:  invokevirtual   #5; //Method java/io/PrintStream.println:(Ljava/lang/String;)V
   14:  goto    0

}

Давайте посмотрим на 2. пример :

package outside;

public class Test {
    public static void main(String[] args) {
        String str;
        while(true){
            str =  String.valueOf(System.currentTimeMillis());
            System.out.println(str);
        }
    }
}

после того, как javac Test.java, javap -c Testвы получите:

public class outside.Test extends java.lang.Object{
public outside.Test();
  Code:
   0:   aload_0
   1:   invokespecial   #1; //Method java/lang/Object."<init>":()V
   4:   return

public static void main(java.lang.String[]);
  Code:
   0:   invokestatic    #2; //Method java/lang/System.currentTimeMillis:()J
   3:   invokestatic    #3; //Method java/lang/String.valueOf:(J)Ljava/lang/String;
   6:   astore_1
   7:   getstatic       #4; //Field java/lang/System.out:Ljava/io/PrintStream;
   10:  aload_1
   11:  invokevirtual   #5; //Method java/io/PrintStream.println:(Ljava/lang/String;)V
   14:  goto    0

}

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

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


3
Это результат JVM Soecification, а не «оптимизация компилятора». Слоты стека, необходимые для метода, все выделяются при входе в метод. Вот как указан байт-код.
Маркиз Лорн

2
@ Архимед, есть еще одна причина, чтобы поместить его в цикл (или просто в блок {{}): компилятор будет повторно использовать память, выделенную в кадре стека для переменной в другой области видимости, если вы объявите в этой другой области некоторые переменные ,
Серж

1
Если его цикл проходит через список объектов данных, то будет ли он иметь какое-то значение для большого объема данных? Вероятно, 40 тысяч.
Митхун Катри

7
Для любого из вас, finalвлюбленных: объявление strкак finalв insideпакете также не имеет значения =)
skia.heliou

27

Объявление объектов в наименьшем объеме улучшает читабельность .

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

Как сказал Дональд Эрвин Кнут :

«Мы должны забыть о малой эффективности, скажем, в 97% случаев: преждевременная оптимизация - корень всего зла»

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


1
«Второй вариант имеет чуть более высокую производительность» => Вы это измерили? Согласно одному из ответов, байт-код один и тот же, поэтому я не вижу, как производительность может отличаться.
assylias

Извините, но это действительно не правильный способ проверить производительность Java-программы (и как вы можете проверить производительность бесконечного цикла в любом случае?)
assylias

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

11

если вы хотите использовать и strвнешнюю петлю; объявить это снаружи. в противном случае вторая версия в порядке.


11

Пожалуйста, перейдите к обновленному ответу ...

Для тех, кто заботится о производительности, удалите System.out и ограничьте цикл до 1 байта. При использовании double (тест 1/2) и использовании String (3/4) истекшее время в миллисекундах приводится ниже для 64-битной Windows 7 Professional и JDK-1.7.0_21. Байт-коды (также приведены ниже для test1 и test2) не совпадают. Мне было лень проверять изменчивые и относительно сложные объекты.

двойной

Тест1 взял: 2710 мсек

Тест2 взял: 2790 мсек

Строка (просто замените double на строку в тестах)

Тест3 взял: 1200 мсек

Test4 взял: 3000 мсек

Компиляция и получение байт-кода

javac.exe LocalTest1.java

javap.exe -c LocalTest1 > LocalTest1.bc


public class LocalTest1 {

    public static void main(String[] args) throws Exception {
        long start = System.currentTimeMillis();
        double test;
        for (double i = 0; i < 1000000000; i++) {
            test = i;
        }
        long finish = System.currentTimeMillis();
        System.out.println("Test1 Took: " + (finish - start) + " msecs");
    }

}

public class LocalTest2 {

    public static void main(String[] args) throws Exception {
        long start = System.currentTimeMillis();
        for (double i = 0; i < 1000000000; i++) {
            double test = i;
        }
        long finish = System.currentTimeMillis();
        System.out.println("Test1 Took: " + (finish - start) + " msecs");
    }
}


Compiled from "LocalTest1.java"
public class LocalTest1 {
  public LocalTest1();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: return

  public static void main(java.lang.String[]) throws java.lang.Exception;
    Code:
       0: invokestatic  #2                  // Method java/lang/System.currentTimeMillis:()J
       3: lstore_1
       4: dconst_0
       5: dstore        5
       7: dload         5
       9: ldc2_w        #3                  // double 1.0E9d
      12: dcmpg
      13: ifge          28
      16: dload         5
      18: dstore_3
      19: dload         5
      21: dconst_1
      22: dadd
      23: dstore        5
      25: goto          7
      28: invokestatic  #2                  // Method java/lang/System.currentTimeMillis:()J
      31: lstore        5
      33: getstatic     #5                  // Field java/lang/System.out:Ljava/io/PrintStream;
      36: new           #6                  // class java/lang/StringBuilder
      39: dup
      40: invokespecial #7                  // Method java/lang/StringBuilder."<init>":()V
      43: ldc           #8                  // String Test1 Took:
      45: invokevirtual #9                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
      48: lload         5
      50: lload_1
      51: lsub
      52: invokevirtual #10                 // Method java/lang/StringBuilder.append:(J)Ljava/lang/StringBuilder;
      55: ldc           #11                 // String  msecs
      57: invokevirtual #9                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
      60: invokevirtual #12                 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
      63: invokevirtual #13                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
      66: return
}


Compiled from "LocalTest2.java"
public class LocalTest2 {
  public LocalTest2();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: return

  public static void main(java.lang.String[]) throws java.lang.Exception;
    Code:
       0: invokestatic  #2                  // Method java/lang/System.currentTimeMillis:()J
       3: lstore_1
       4: dconst_0
       5: dstore_3
       6: dload_3
       7: ldc2_w        #3                  // double 1.0E9d
      10: dcmpg
      11: ifge          24
      14: dload_3
      15: dstore        5
      17: dload_3
      18: dconst_1
      19: dadd
      20: dstore_3
      21: goto          6
      24: invokestatic  #2                  // Method java/lang/System.currentTimeMillis:()J
      27: lstore_3
      28: getstatic     #5                  // Field java/lang/System.out:Ljava/io/PrintStream;
      31: new           #6                  // class java/lang/StringBuilder
      34: dup
      35: invokespecial #7                  // Method java/lang/StringBuilder."<init>":()V
      38: ldc           #8                  // String Test1 Took:
      40: invokevirtual #9                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
      43: lload_3
      44: lload_1
      45: lsub
      46: invokevirtual #10                 // Method java/lang/StringBuilder.append:(J)Ljava/lang/StringBuilder;
      49: ldc           #11                 // String  msecs
      51: invokevirtual #9                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
      54: invokevirtual #12                 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
      57: invokevirtual #13                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
      60: return
}

ОБНОВЛЕННЫЙ ОТВЕТ

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

  1. Некоторые подробности в блоге: следует ли объявлять переменную внутри цикла или перед циклом?
  2. Репозиторий GitHub: https://github.com/gunduru/jvdt
  3. Результаты теста для двойного регистра и цикла 100M (и да всех деталей JVM): https://microbenchmarks.appspot.com/runs/b1cef8d1-0e2c-4120-be61-a99faff625b4

Объявлено до 1759,209 Объявлено внутри 2 242 308

  • Объявлено до 1759.209 нс
  • Объявлено внутри 2 242 308 нс

Частичный тестовый код для двойной декларации

Это не идентично коду выше. Если вы просто кодируете фиктивный цикл, JVM пропускает его, так что по крайней мере вам нужно что-то присвоить и вернуть. Это также рекомендуется в документации суппорта.

@Param int size; // Set automatically by framework, provided in the Main
/**
* Variable is declared inside the loop.
*
* @param reps
* @return
*/
public double timeDeclaredInside(int reps) {
    /* Dummy variable needed to workaround smart JVM */
    double dummy = 0;

    /* Test loop */
    for (double i = 0; i <= size; i++) {

        /* Declaration and assignment */
        double test = i;

        /* Dummy assignment to fake JVM */
        if(i == size) {
            dummy = test;
        }
    }
    return dummy;
}

/**
* Variable is declared before the loop.
*
* @param reps
* @return
*/
public double timeDeclaredBefore(int reps) {

    /* Dummy variable needed to workaround smart JVM */
    double dummy = 0;

    /* Actual test variable */
    double test = 0;

    /* Test loop */
    for (double i = 0; i <= size; i++) {

        /* Assignment */
        test = i;

        /* Not actually needed here, but we need consistent performance results */
        if(i == size) {
            dummy = test;
        }
    }
    return dummy;
}

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


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

1
@EJP Это должно быть довольно ясно для тех, кто интересуется этой темой. Методология взята из ответа PrimosK для предоставления более полезной информации. Если честно, я понятия не имею, как улучшить этот ответ, может быть, вы можете нажать кнопку редактирования и показать нам, как это сделать правильно?
Онур Гюндуру

2
1) Байт-код Java оптимизируется (переупорядочивается, сворачивается и т. Д.) Во время выполнения, поэтому не стоит слишком беспокоиться о том, что написано в файлах .class. 2) есть 1.000.000.000 прогонов, чтобы получить выигрыш в производительности за 2.8 с, то есть около 2.8 нс за цикл против безопасного и правильного стиля программирования. Явный победитель для меня. 3) Поскольку вы не предоставляете никакой информации о разминке, ваши сроки совершенно бесполезны.
Закодировано

@ Лучшие тестовые тесты / микро тестирование с помощью штангенциркуля только для двойных и 100-миллиметровых циклов. Результаты онлайн, если вы хотите, чтобы другие случаи не стеснялись редактировать.
Онур Гюндуру

Спасибо, это устраняет пункты 1) и 3). Но даже если время выросло до ~ 5 нс за цикл, это все равно время, которое нужно игнорировать. В теории есть небольшой потенциал оптимизации, в действительности то, что вы делаете за цикл, обычно намного дороже. Таким образом, потенциал будет максимум несколько секунд за несколько минут или даже часов. Существуют и другие варианты с более высоким потенциалом (например, Fork / Join, параллельные потоки), которые я бы проверил, прежде чем тратить время на подобные низкоуровневые оптимизации.
Закодировано

7

Одним из решений этой проблемы может быть предоставление переменной области, инкапсулирующей цикл while:

{
  // all tmp loop variables here ....
  // ....
  String str;
  while(condition){
      str = calculateStr();
      .....
  }
}

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



5

Если вам не нужно использовать strцикл while (связанный с областью действия), тогда второе условие, т.е.

  while(condition){
        String str = calculateStr();
        .....
    }

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


2
Обратите внимание, что даже в первом варианте объект не создается, если условие ложно.
Филипп Вендлер

@ Филипп: Да, вы правы. Виноват. Я думал, как сейчас. Что ты думаешь?
Cratylus

1
Что ж, «определение объекта в стеке» - это несколько странный термин в мире Java. Кроме того, выделение переменной в стеке обычно является циклом во время выполнения, так зачем беспокоиться? Возможность помочь программисту - настоящая проблема.
Филипп Вендлер

3

Я думаю, что лучшим ресурсом для ответа на ваш вопрос будет следующий пост:

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

Насколько я понимаю, эта вещь будет зависеть от языка. IIRC Java оптимизирует это, так что нет никакой разницы, но JavaScript (например) будет распределять всю память каждый раз в цикле. В частности, в Java, я думаю, вторая будет работать быстрее, когда будет выполнено профилирование.


3

Как многие люди указали,

String str;
while(condition){
    str = calculateStr();
    .....
}

это НЕ лучше , чем это:

while(condition){
    String str = calculateStr();
    .....
}

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


1
кроме, вероятно, таким образом: ссылка
Дайний Крейвис

2

Объявление String str вне цикла wile позволяет ссылаться на него внутри и снаружи цикла while. Объявление String str внутри цикла while позволяет ссылаться на него только внутри цикла while.




1

strПеременная будет доступна и зарезервировали место в памяти даже после того, как в то время как выполняется ниже кода.

 String str;
    while(condition){
        str = calculateStr();
        .....
    }

strПеременная не будет доступна , а также память будет выпущен , который был выделен strпеременной в коде ниже.

while(condition){
    String str = calculateStr();
    .....
}

Если мы последуем за вторым, это наверняка уменьшит нашу системную память и увеличит производительность.


0

Объявление внутри цикла ограничивает область действия соответствующей переменной. Все зависит от требований проекта к области действия переменной.


0

Действительно, вопрос, изложенный выше, является проблемой программирования. Как бы вы хотели запрограммировать свой код? Где вам нужен STR для доступа? Нет смысла объявлять переменную, которая используется локально как глобальная переменная. Основы программирования я считаю.


-1

Эти два примера приводят к одному и тому же. Однако первый предоставляет вам возможность использовать strпеременную вне цикла while; второй нет.


-1

Предупреждение для почти всех в этом вопросе: вот пример кода, где внутри цикла он может быть в 200 раз медленнее на моем компьютере с Java 7 (и потребление памяти также немного отличается). Но речь идет о распределении, а не только сфере.

public class Test
{
    private final static int STUFF_SIZE = 512;
    private final static long LOOP = 10000000l;

    private static class Foo
    {
        private long[] bigStuff = new long[STUFF_SIZE];

        public Foo(long value)
        {
            setValue(value);
        }

        public void setValue(long value)
        {
            // Putting value in a random place.
            bigStuff[(int) (value % STUFF_SIZE)] = value;
        }

        public long getValue()
        {
            // Retrieving whatever value.
            return bigStuff[STUFF_SIZE / 2];
        }
    }

    public static long test1()
    {
        long total = 0;

        for (long i = 0; i < LOOP; i++)
        {
            Foo foo = new Foo(i);
            total += foo.getValue();
        }

        return total;
    }

    public static long test2()
    {
        long total = 0;

        Foo foo = new Foo(0);
        for (long i = 0; i < LOOP; i++)
        {
            foo.setValue(i);
            total += foo.getValue();
        }

        return total;
    }

    public static void main(String[] args)
    {
        long start;

        start = System.currentTimeMillis();
        test1();
        System.out.println(System.currentTimeMillis() - start);

        start = System.currentTimeMillis();
        test2();
        System.out.println(System.currentTimeMillis() - start);
    }
}

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

Просто сказать, что иногда снаружи или внутри цикла имеет значение.


1
Конечно, второе быстрее, но вы делаете разные вещи: test1 создает много Foo-объектов с большими массивами, test2 нет. test2 снова и снова использует один и тот же объект Foo, что может быть опасно в многопоточных средах.
Закодировано

Опасен в многопоточной среде ??? Пожалуйста, объясните почему. Мы говорим о локальной переменной. Он создается при каждом вызове метода.
RT15

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

Ps: Ваш SetValue метод должен быть bigStuff[(int) (value % STUFF_SIZE)] = value;(Попробуйте значение 2147483649L)
HARDCODED

Говоря о побочных эффектах: сравнивали ли вы результаты своих методов?
Закодировано

-1

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


-2

У вас есть риск того, NullPointerExceptionчто ваш calculateStr()метод вернет null, а затем вы попытаетесь вызвать метод на str.

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


2
Это никак не связано с вопросом. Вероятность исключения NullPointerException (при будущих вызовах функций) не будет зависеть от того, как объявлена ​​переменная.
Пустыня льда

1
Я так не думаю, потому что вопрос «Каков наилучший способ сделать это?». ИМХО Я бы предпочел более безопасный код.
Реми Дулаге

1
Существует нулевой риск того, что NullPointerException.при попытке этого кода return str;возникнет ошибка компиляции.
маркиз Лорн
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.