Какой цикл имеет лучшую производительность? Зачем? - PullRequest
50 голосов
/ 21 сентября 2008
String s = "";
for(i=0;i<....){
    s = some Assignment;
}

или

for(i=0;i<..){
    String s = some Assignment;
}

Мне больше не нужно использовать 's' вне цикла. Первый вариант, возможно, лучше, так как новая строка не инициализируется каждый раз. Второе, однако, приведет к тому, что область действия переменной будет ограничена самим циклом.

РЕДАКТИРОВАТЬ: В ответ на ответ Milhous. Было бы бессмысленно присваивать String константу внутри цикла, не так ли? Нет, здесь «некоторое назначение» означает изменяющееся значение, полученное из списка, который повторяется.

Кроме того, вопрос не в том, что я беспокоюсь об управлении памятью. Просто хочу узнать, что лучше.

Ответы [ 8 ]

107 голосов
/ 21 сентября 2008

Ограниченная сфера - лучший

Используйте ваш второй вариант:

for ( ... ) {
  String s = ...;
}

Область действия не влияет на производительность

Если вы дизассемблируете код, скомпилированный из каждого (с помощью инструмента JDK javap), вы увидите, что цикл компилируется с одинаковыми инструкциями JVM в обоих случаях. Также обратите внимание, что Брайана Р. Бонди "Вариант № 3" идентичен Варианту № 1. При использовании более узкой области видимости ничего не добавляется и не удаляется из стека, и в обоих случаях в стеке используются одни и те же данные.

Избегать преждевременной инициализации

Единственное различие между этими двумя случаями состоит в том, что в первом примере переменная s инициализируется без необходимости. Это отдельная проблема от расположения объявления переменной. Это добавляет две потраченные впустую инструкции (загрузить строковую константу и сохранить ее в слоте стекового фрейма). Хороший инструмент статического анализа предупредит вас, что вы никогда не читаете значение, которое вы присваиваете s, а хороший JIT-компилятор, вероятно, исключит его во время выполнения.

Вы можете исправить это, просто используя пустое объявление (т.е. String s;), но это считается плохой практикой и имеет другой побочный эффект, который обсуждается ниже.

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

Сохранение слотов стека

Как уже упоминалось, в то время как инструкции JVM одинаковы в обоих случаях, существует тонкий побочный эффект, который позволяет на уровне JVM использовать максимально ограниченную возможную область действия. Это видно в «таблице локальных переменных» для метода. Подумайте, что произойдет, если у вас несколько циклов с переменными, объявленными в неоправданно большом объеме:

void x(String[] strings, Integer[] integers) {
  String s;
  for (int i = 0; i < strings.length; ++i) {
    s = strings[0];
    ...
  }
  Integer n;
  for (int i = 0; i < integers.length; ++i) {
    n = integers[i];
    ...
  }
}

Переменные s и n могут быть объявлены внутри их соответствующих циклов, но так как они не являются, компилятор использует два «слота» в кадре стека. Если они были объявлены внутри цикла, компилятор может повторно использовать один и тот же слот, уменьшая размер стека.

Что на самом деле имеет значение

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

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

22 голосов
/ 21 сентября 2008

В теории это пустая трата ресурсов для объявления строки внутри цикла. В практике , однако, оба представленных фрагмента будут скомпилированы в один и тот же код (объявление вне цикла).

Итак, если ваш компилятор выполняет какую-либо оптимизацию, разницы нет.

17 голосов
/ 21 сентября 2008

В общем, я бы выбрал второе, потому что область действия переменной 's' ограничена циклом. Преимущества:

  • Это лучше для программиста, потому что вам не нужно беспокоиться о том, что 's' будет снова использовано где-то позже в функции
  • Это лучше для компилятора, поскольку область действия переменной меньше, и поэтому он потенциально может выполнять больше анализа и оптимизации
  • Это лучше для будущих читателей, потому что они не будут удивляться, почему переменная 's' объявляется вне цикла, если она никогда не используется позже
6 голосов
/ 21 сентября 2008

Если вы хотите ускорить циклы, я предпочитаю объявлять переменную max рядом со счетчиком, чтобы не требовалось повторных поисков для условия:

вместо

for (int i = 0; i < array.length; i++) {
  Object next = array[i];
}

Я предпочитаю

for (int i = 0, max = array.lenth; i < max; i++) {
  Object next = array[i];
}

Любые другие вещи, которые следует учитывать, уже были упомянуты, так что просто мои два цента (см. Пост Эриксона)

Greetz, GHad

4 голосов
/ 21 сентября 2008

Чтобы добавить немного к @ ответу Эстебана Арайи , они оба будут требовать создания новой строки каждый раз в цикле (в качестве возвращаемого значения выражения some Assignment). Эти строки нужно собирать мусором в любом случае.

3 голосов
/ 14 апреля 2009

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

Я заметил, просматривая исходный код Java, что некоторые методы, такие как String.contentEquals (дублируется ниже), создают избыточные локальные переменные, которые являются просто копиями переменных класса. Я считаю, что где-то был комментарий, который подразумевал, что доступ к локальным переменным происходит быстрее, чем доступ к переменным класса.

В этом случае «v1» и «v2», по-видимому, не нужны и могут быть исключены для упрощения кода, но были добавлены для повышения производительности.

public boolean contentEquals(StringBuffer sb) {
    synchronized(sb) {
        if (count != sb.length())
            return false;
        char v1[] = value;
        char v2[] = sb.getValue();
        int i = offset;
        int j = 0;
        int n = count;
        while (n-- != 0) {
            if (v1[i++] != v2[j++])
                return false;
        }
    }
    return true;
}
1 голос
/ 30 июля 2013

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

Process one;
BufferedInputStream two;
try{
one = Runtime.getRuntime().exec(command);
two = new BufferedInputStream(one.getInputStream());
}
}catch(e){
e.printstacktrace
}
finally{
//null to ensure they are erased
one = null;
two = null;
//nudge the gc
System.gc();
}
1 голос
/ 21 сентября 2008

Мне кажется, что нам нужно больше подробностей о проблеме.

s = some Assignment;

не указано, что это за назначение. Если присвоение

s = "" + i + "";

тогда нужно назначить новое жало.

но если это

s = some Constant;

s будет просто указывать на область памяти констант, и, таким образом, первая версия будет более эффективно использовать память.

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

...