Какой цикл имеет лучшую производительность?Почему?
-
02-07-2019 - |
Вопрос
String s = "";
for(i=0;i<....){
s = some Assignment;
}
или
for(i=0;i<..){
String s = some Assignment;
}
Мне больше не нужно использовать 's' вне цикла.Первый вариант, пожалуй, лучше, поскольку новая строка не инициализируется каждый раз.Однако второй вариант приведет к тому, что область действия переменной будет ограничена самим циклом.
РЕДАКТИРОВАТЬ:В ответ на ответ Милхауса.Было бы бессмысленно присваивать строку константе внутри цикла, не так ли?Нет, здесь «некоторое присвоение» означает изменяющееся значение, полученное из перебираемого списка.
Кроме того, вопрос не в том, что меня беспокоит управление памятью.Просто хочу знать, что лучше.
Решение
Ограниченный объем лучше всего
Используйте второй вариант:
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-компилятор увидит, что невозможно прочитать начальное значение, которое вы расточительно присваиваете, и оптимизирует назначение.Сохранение слота здесь или там не повлияет на ваше приложение.
Важно сделать ваш код читаемым и простым в обслуживании, и в этом отношении использование ограниченной области явно лучше.Чем меньшую область действия имеет переменная, тем легче понять, как она используется и какое влияние окажут любые изменения в коде.
Другие советы
В теория, объявление строки внутри цикла — это пустая трата ресурсов.В упражняться, однако оба представленных вами фрагмента будут скомпилированы в один и тот же код (объявление вне цикла).
Итак, если ваш компилятор выполняет какую-либо оптимизацию, разницы нет.
В общем, я бы выбрал второй, потому что область действия переменной 's' ограничена циклом.Преимущества:
- Это лучше для программиста, потому что вам не нужно беспокоиться о том, что где-то позже в функции снова будет использоваться 's'.
- Это лучше для компилятора, поскольку область действия переменной меньше, и поэтому он потенциально может выполнять больший анализ и оптимизацию.
- Это лучше для будущих читателей, потому что они не будут задаваться вопросом, почему переменная 's' объявлена вне цикла, если она никогда не будет использоваться позже.
Если вы хотите ускорить циклы, я предпочитаю объявлять переменную 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];
}
Любые другие вещи, которые следует учитывать, уже были упомянуты, так что только мои два цента (см. пост Эриксона)
Гретц, Гад
Добавлю немного к @Ответ Эстебана Арайи, они оба потребуют создания новой строки каждый раз в цикле (в качестве возвращаемого значения some Assignment
выражение).Эти строки в любом случае должны быть очищены от мусора.
Я знаю, что это старый вопрос, но я решил добавить немного, что немного связанный.
Просматривая исходный код 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;
}
Мне кажется, что нам нужна более конкретизация проблемы.
А
s = some Assignment;
Что это за задание, не уточняется.Если задание
s = "" + i + "";
тогда необходимо выделить новое жало.
но если это так
s = some Constant;
s будет просто указывать на местоположение констант в памяти, и, таким образом, первая версия будет более эффективной с точки зрения использования памяти.
Кажется, мне немного глупо беспокоиться о значительной оптимизации цикла for для интерпретируемого языка, ИМХО.
Когда я использую несколько потоков (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();
}