Декларування змінних всередині або зовні циклу


236

Чому наступне працює добре?

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

Але цей, як кажуть, небезпечний / неправильний:

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

Чи потрібно оголошувати змінні поза циклом?

Відповіді:


289

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

У вашому прикладі я припускаю, що strвін не використовується за межами whileциклу, інакше ви не ставите запитання, тому що оголосити його всередині whileциклу не було б варіантом, оскільки він не збирався б.

Отже, оскільки strвін не використовується за межами циклу, найменший можливий обсяг для strзнаходиться в циклі while.

Отже, відповідь наголошується на тому, що strабсолютно слід оголосити в циклі while. Ніяких ifs, no ands, no buts.

Єдиний випадок, коли це правило може бути порушене, це якщо з якихось причин має життєво важливе значення, щоб кожен тактовий цикл був витіснений з коду, і в цьому випадку ви можете розглянути можливість інстанції чогось у зовнішній області і повторно використовувати його замість реінстанціювати його на кожній ітерації внутрішнього простору. Однак це не стосується вашого прикладу, через незмінність рядків у java: новий екземпляр str завжди створюватиметься на початку вашого циклу, і його доведеться викинути в кінці, тому там немає можливості оптимізуватись там.

EDIT: (вводячи мій коментар нижче у відповідь)

У будь-якому випадку, правильний спосіб зробити це - правильно записати весь свій код, встановити вимогу щодо продуктивності свого продукту, відміряти свій кінцевий продукт відповідно до цієї вимоги, а якщо він його не задовольняє, то йдіть на оптимізацію речей. І зазвичай це відбувається - це те, що ви знайдете способи забезпечити приємні та формальні алгоритмічні оптимізації лише в декількох місцях, завдяки чому наша програма відповідає її вимогам щодо продуктивності, а не переглядати всю вашу кодову базу та налаштовувати та ламати речі. щоб вичавити тактові тактові годинники тут і там.


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
Це результат Soecification JVM, а не «оптимізація компілятора». Усі слоти для стека, необхідні методу, всі виділяються при вході в метод. Ось як вказано байт-код.
Маркіз Лорн

2
@Arhimed Є ще одна причина, щоб помістити його всередину циклу (або просто блоку '{}'): компілятор повторно використовувати пам'ять, виділену в кадрі стека, для змінної в іншій області, якщо ви оголосите в цій іншій області деяку понад змінну .
Серж

1
Якщо його цикл через список об'єктів даних, то чи матиме це значення для масової інформації? Напевно, 40 тисяч.
Мітхун Хатрі

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

27

Оголошення об’єктів у найменшому обсязі покращує читабельність .

Продуктивність не має значення для сьогоднішніх компіляторів. (У цьому сценарії)
З точки зору обслуговування, другий варіант кращий.
Декларуйте та ініціалізуйте змінні там же, у найкоротшому обсязі.

Як розповів Дональд Ервін Кнут :

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

тобто) ситуація, коли програміст дозволяє врахувати ефективність впливу на дизайн фрагмента коду. Це може привести до конструкції, яка не так чистий , як це могло б бути або код , який є неправильним, так як код ускладнюється по оптимізації і програміст відволікається оптимізації .


1
"2-й варіант має трохи швидші показники" => Ви його виміряли? Згідно з однією з відповідей, байт-код однаковий, тому я не бачу, як продуктивність може бути різною.
ассілія

Вибачте, але це насправді не правильний спосіб перевірити продуктивність програми java (а як ви все-таки можете перевірити продуктивність нескінченного циклу?)
assylias

Я погоджуюся з вашими іншими пунктами - я просто вважаю, що різниці в ефективності немає.
assylias

11

якщо ви також хочете використовувати strзовнішнє цикл; оголосити це зовні. в іншому випадку 2-а версія - це добре.


11

Перейдіть до оновленої відповіді ...

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

подвійний

Тест1 зайняв: 2710 мс

Тест2 зайняв: 2790 мс

Рядок (просто замініть подвійний на рядок у тестах)

Test3 зайняв: 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

Задекларовано до 1,759.209 ДекларованоВнутрі 2442,3030

  • Задекларовано до 1759,209 нс
  • Задекларовано Внутрі 2242.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 для надання більш корисної інформації. Якщо чесно, я не знаю, як покращити цю відповідь, можливо, ви можете натиснути редагувати та показати нам, як це зробити правильно?
Onur Günduru

2
1) Java Bytecode оптимізується (переупорядковується, згортається тощо) під час виконання, тому не переймайтеся тим, що написано у файлах .class. 2) Є 1.000.000.000 прогонів, щоб отримати виграш у продуктивності в 2,8s, так що приблизно 2,8ns за запуск проти безпечного та правильного стилю програмування. Явний переможець для мене. 3) Оскільки ви не надаєте жодної інформації про розминку, ваші таймінги є марними.
Жорстко кодований

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

Дякую, це виключає пункти 1) та 3). Але навіть якщо час піднімався до ~ 5нс за цикл, це все-таки час, який слід ігнорувати. Теоретично існує невеликий потенціал оптимізації, насправді речі, які ви робите за цикл, зазвичай набагато дорожчі. Тож потенціал мав би становити максимум кілька секунд протягом декількох хвилин або навіть годин. Є й інші варіанти з більшим потенціалом (наприклад, Fork / Join, паралельні потоки), які я б перевірив, перш ніж витрачати час на такі низькі рівні оптимізації.
Твердо кодований

7

Одним з варіантів вирішення цієї проблеми може стати надання змінної області, що інкапсулює цикл while:

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

Вони будуть автоматично де-посилатися, коли закінчується зовнішня область.



5

Якщо вам не потрібно використовувати strцикл after time (пов'язаний зі сферою), то друга умова, тобто

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

краще, оскільки якщо ви визначаєте об'єкт у стеці, лише якщо conditionце правда. Тобто використовуйте його, якщо вам це потрібно


2
Зауважте, що навіть у першому варіанті жоден об'єкт не будується, якщо умова хибна.
Філіп Вендлер

@ Філіп: Так, ти маєш рацію. Моє ліжко. Я думав, як зараз. Що ви думаєте?
Кратилій

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. Оголошення рядка 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-Object до операції, яка обробляє дані асинхронно, операція все ще може працювати над примірником Foo, коли ви змінюєте в ньому дані. Це навіть не повинно бути багатопоточним, щоб мати побічні ефекти. Тож повторне використання екземплярів є досить небезпечним, коли ви не знаєте, хто все ще використовує цей екземпляр
Hardcoded

Ps: Ваш метод setValue має бути bigStuff[(int) (value % STUFF_SIZE)] = value;(Спробуйте значення 2147483649L)
жорсткий код

Якщо говорити про побічні ефекти: Ви порівняли результати своїх методів?
Твердо кодований

-1

Я думаю, що розмір об'єкта також має значення. В одному з моїх проектів ми оголосили і ініціалізували великий двовимірний масив, який змушував програму викидати виняток із пам'яті. Ми замість цього перемістили декларацію з циклу та очистили масив на початку кожної ітерації.


-2

У вас є ризик, NullPointerExceptionякщо ваш calculateStr()метод поверне нульове значення і тоді ви спробуєте викликати метод на str.

Загалом, уникайте змінних з нульовим значенням. Це до речі сильніше для атрибутів класу.


2
Це жодним чином не пов'язане з питанням. Імовірність NullPointerException (на майбутніх викликах функцій) не залежатиме від того, як оголошена змінна.
Пустельний лід

1
Я не думаю, що це питання, оскільки питання "Який найкращий спосіб це зробити?" ІМХО я вважаю за краще безпечний код.
Ремі Дулаеге

1
Існує нульовий ризик, NullPointerException.якщо цей код спробував би return str;його, виникне помилка компіляції.
Маркіз Лорн
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.