Разница между объявлением переменных до или в цикле?
Я всегда задавался вопросом, если, в общем, объявление выбрасываемой переменной перед циклом, в отличие от многократно внутри цикла, делает какую-либо (производительность) разницу?
А (совершенно бессмысленно) пример в Java:
a) объявление перед циклом:
double intermediateResult;
for(int i=0; i < 1000; i++){
intermediateResult = i;
System.out.println(intermediateResult);
}
b) объявление (неоднократно) внутри цикла:
for(int i=0; i < 1000; i++){
double intermediateResult = i;
System.out.println(intermediateResult);
}
какой из них лучше,a или b?
Я подозреваю, что повторное объявление переменной (пример b) создает больше накладных расходов в теории, но это компиляторы достаточно умны, так что это не имеет значения. Пример b имеет преимущество быть более компактным и ограничение области видимости переменной, где она используется. Тем не менее, я склонен кодировать в соответствии с примером a.
Edit: меня особенно интересует случай Java.
24 ответов:
Что лучше, a или b?
с точки зрения производительности, вы должны измерить его. (И на мой взгляд, если вы можете измерить разницу, компилятор не очень хорошо).
с точки зрения обслуживания, b лучше. Объявить и инициализировать переменные в одном месте, в максимально узкой области. Не оставляйте зияющую дыру между объявлением и инициализацией и не загрязняйте пространства имен не надо.
Ну я пробежал ваши примеры A и B по 20 раз каждый, зацикливаясь 100 миллионов раз.(JVM-1.5.0)
A: среднее время выполнения: .074 сек
B: среднее время выполнения : .067 секунды
к моему удивлению B был немного быстрее. Так быстро, как компьютеры сейчас трудно сказать, если вы могли бы точно измерить эту. Я бы тоже закодировал его, но я бы сказал, что это не имеет значения.
Это зависит от языка и точного использования. Например, в 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переменной.
вот что я написал и скомпилировал в. 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 / byte и во время выполнения преобразуется в машинный язык. Таким образом, на машинном языке двойник может даже не быть создан в стеке. Это может быть просто зарегистрировать код подумайте, что это временная переменная для
Это попался 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...
Я сделал простой тест:
int b; for (int i = 0; i < 10; i++) { b = i; }vs
for (int i = 0; i < 10; i++) { int b = i; }я скомпилировал эти коды с помощью GCC - 5.2.0. А потом я разобрал главное () из этих двух кодов и вот результат:
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>: retvs
2
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которые exaclty тот же результат asm. разве это не доказательство того, что два кода производят одно и то же?
Я всегда буду использовать A (вместо того, чтобы полагаться на компилятор) и также могу переписать:
for(int i=0, double intermediateResult=0; i<1000; i++){ intermediateResult = i; System.out.println(intermediateResult); }Это все еще ограничивает
intermediateResultв область действия цикла, но не объявляется повторно во время каждой итерации.
Это зависит от языка-IIRC C# оптимизирует это, поэтому нет никакой разницы, но JavaScript (например) будет делать все выделение памяти shebang каждый раз.
на мой взгляд, b-лучшая структура. В a последнее значение intermediateResult остается после завершения цикла.
изменить: Это не имеет большого значения для типов значений, но ссылочные типы могут быть несколько весомыми. Лично мне нравится, чтобы переменные были разыменованы как можно скорее для очистки, и b делает это для вас,
Я подозреваю, что несколько компиляторов могут оптимизировать оба, чтобы быть одним и тем же кодом, но, конечно, не все. Поэтому я бы сказал, что вам лучше с первым. Единственная причина для последнего, если вы хотите, чтобы убедиться, что объявленная переменная используется только в пределах вашего цикла.
Как правило, я объявляю свои переменные во внутренней максимально возможной области. Итак, если вы не используете intermediateResult вне цикла, то я бы пошел с B.
сотрудник предпочитает первую форму, говоря, что это оптимизация, предпочитая повторно использовать объявление.
Я предпочитаю второй (и попытаться убедить моего коллегу! ;-)), прочитав, что:
- это уменьшает область переменных туда, где они необходимы, что является хорошей вещью.
- Java оптимизирует достаточно, чтобы не сделать никакой существенной разницы в производительности. IIRC, возможно, вторая форма еще быстрее.
в любом случае, она падает в категория преждевременной оптимизации, которая зависит от качества компилятора и / или JVM.
есть разница в C# , если вы используете переменную в лямбде и т. д. Но в целом компилятор будет в основном делать то же самое, предполагая, что переменная используется только в цикле.
учитывая, что они в основном одинаковы: обратите внимание, что версия b делает ее гораздо более очевидной для читателей, что переменная не используется и не может использоваться после цикла. Кроме того, версия b гораздо легче рефакторинг. Более трудно извлечь тело петли в свое собственный метод в версии a. кроме того, версия b уверяет вас, что нет никакого побочного эффекта для такого рефакторинга.
следовательно, версия раздражает меня нет конца, потому что нет никакой выгоды и это делает его гораздо труднее рассуждать о коде...
Ну, вы всегда можете сделать область для этого:
{ //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); } }таким образом, вы объявляете переменную только один раз, и она умрет, когда вы покинете цикл.
Я всегда думал, что если вы объявляете свои переменные внутри вашего цикла, то вы тратите впустую память. Если у вас есть что-то вроде этого:
for(;;) { Object o = new Object(); }тогда не только объект должен быть создан для каждой итерации, но должна быть новая ссылка, выделенная для каждого объекта. Кажется, что если сборщик мусора работает медленно, то у вас будет куча висячих ссылок, которые нужно очистить.
однако, если у вас есть это:
Object o; for(;;) { o = new Object(); }тогда вы создаете только одну ссылку и назначаете ей новый объект каждый раз. Конечно, это может занять немного больше времени, чтобы выйти за рамки, но тогда есть только одна висячая ссылка для решения.
моя практика следующая:
Если тип переменной-это просто (int, double,...) Я предпочитаю вариант b (внутри).
причина: сокращение объема переменной.Если тип переменной не просто (какой-то
classилиstruct) Я предпочитаю вариант a (снаружи).
причина: уменьшение количества ctor-dtor звонки.
С точки зрения производительности, снаружи (намного) лучше.
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 миллисекунд. внутри() занимает 1,5 секунды.
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? }вы наверняка столкнетесь с проблемами утечки памяти!. Поэтому я считаю, что " A "является более безопасной ставкой, в то время как" B " уязвим для накопления памяти esp, работающей с близкими исходными библиотеками.Вы можете проверить usinng 'Valgrind' инструмент на Linux специально sub tool 'Helgrind'.
Это интересный вопрос. Из моего опыта есть окончательный вопрос, чтобы рассмотреть, когда вы обсуждаете этот вопрос для кода:
есть ли причина, по которой переменная должна быть глобальной?
имеет смысл объявлять переменную только один раз, глобально, а не много раз локально, потому что это лучше для организации кода и требует меньше строк кода. Однако, если он должен быть объявлен локально только в одном методе, I инициализирует его в этом методе, поэтому ясно, что переменная исключительно относится к этому методу. Будьте осторожны, чтобы не вызывать эту переменную вне метода, в котором она инициализируется, если вы выберете последний вариант-ваш код не будет знать, о чем вы говорите, и сообщит об ошибке.
кроме того, в качестве примечания, не дублируйте имена локальных переменных между различными методами, даже если их цели почти идентичны; это просто сбивает с толку.
Я тестировал для JS с узлом 4.0.0, если кто-то заинтересован. Объявление вне цикла привело к ~.5 мс повышение производительности в среднем более 1000 испытаний с 100 миллионов итераций цикла в пробе. Поэтому я собираюсь сказать, что вы можете написать его самым читаемым / ремонтопригодным способом, который является B, imo. Я бы поставил свой код в скрипку, но я использовал модуль узла performance-now. Вот код:
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)
Это лучшая форма
double intermediateResult; int i = byte.MinValue; for(; i < 1000; i++) { intermediateResult = i; System.out.println(intermediateResult); }1) таким образом объявляется один раз как переменная, так и не каждая для цикла. 2) назначение это жирнее, чем все остальные варианты. 3) Таким образом, правило bestpractice-это любое объявление вне итерации.
попробовал то же самое в Go и сравнил вывод компилятора с помощью
go tool compile -Sс go 1.9.4нулевая разница, согласно выходу ассемблера.
даже если я знаю, что мой компилятор достаточно умен, я не буду полагаться на него и буду использовать вариант a).
вариант b) имеет смысл для меня только в том случае, если вам отчаянно нужно сделать intermediateResult недоступно после тела цикла. Но я все равно не могу представить себе такой отчаянной ситуации....
EDIT:Джон Скит сделал очень хороший момент, показывая, что объявление переменной внутри цикла может сделать фактическую семантическую разницу.
Comments