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

<p>я всегда задавался вопросом, имеет ли какое-либо значение (производительность), вообще говоря, объявление одноразовой переменной перед циклом, в отличие от многократного внутри цикла? a <em>(совершенно бессмысленно)</em> пример на java:</p><p><strong> а) </strong>объявление перед циклом:</p><pre><code>double intermediateresult; for(int i=0; i < 1000; i++){ intermediateresult = i; system.out.println(intermediateresult); } </code></pre><p><strong> б) </strong>объявление (повторно) внутри цикла:</p><pre><code>for(int i=0; i < 1000; i++){ double intermediateresult = i; system.out.println(intermediateresult); } </code></pre><p>какой из них лучше,<strong> a </strong>или<strong> b </strong>? </p><p>я подозреваю, что повторное объявление переменной (пример<strong> b </strong>) создает дополнительные издержки <em>в теории</em>, но компиляторы достаточно умны, чтобы это не имело значения. пример<strong> b </strong>имеет преимущество в том, что он более компактен и ограничивает область действия переменной тем местом, где она используется. тем не менее, я склонен кодировать в соответствии с примером<strong> a </strong>.</p><p><strong> <em>edit:</em> </strong>меня особенно интересует случай с java.</p>

<p>что лучше,<strong> а </strong>или<strong> б </strong>?</p><p>с точки зрения производительности, вам придется его измерять. (и, по моему мнению, если вы можете измерить разницу, компилятор не очень хорош).</p><p>с точки зрения обслуживания,<strong> b </strong>лучше. объявите и инициализируйте переменные в одном и том же месте, в самой узкой области видимости. не оставляйте зазор между объявлением и инициализацией и не загрязняйте пространства имен, которые вам не нужны.</p>

<p>ну, я запускал ваши примеры a и b по 20 раз каждый, повторяя 100 миллионов раз (jvm - 1.5.0).</p><p>a: среднее время выполнения: 0,074 сек.</p><p>b: среднее время выполнения: 0,067 сек.</p><p>к моему удивлению, b был немного быстрее .. с такой скоростью, как компьютеры, сейчас трудно сказать, если бы вы могли точно измерить это ...\_\_ ,.</p>

<p>это зависит от языка и точного использования. например, в c # 1 это не имело никакого значения. в c # 2, если локальная переменная захвачена анонимным методом (или лямбда-выражением в c # 3), это может иметь очень важное значение.</p><p>пример:</p><pre><code>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(); } } } </code></pre><p>результат:</p><pre><code>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 </code></pre><p>разница в том, что все действия захватывают одну и ту же переменную <code>outer</code>, но у каждого есть своя отдельная переменная <code>inner</code>.</p>

<p>вот что я написал и скомпилировал в .net.</p><pre><code>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); } </code></pre><p>это то, что я получаю из <a href="http://en.wikipedia.org/wiki/.net\_reflector">.net reflector</a> когда <a href="http://en.wikipedia.org/wiki/common\_intermediate\_language">cil</a> возвращается в код.</p><pre><code>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); } </code></pre><p>так что после компиляции оба выглядят одинаково. в управляемых языках код преобразуется в cl/байт-код, а во время исполнения он преобразуется в машинный язык. таким образом, на машинном языке двойка может даже не быть создана в стеке. это может быть просто регистр, поскольку код отражает, что это временная переменная для функции <code>writeline</code>. существует целый набор правил оптимизации только для циклов. так что средний парень не должен беспокоиться об этом, особенно в управляемых языках. есть случаи, когда вы можете оптимизировать управление кодом, например, если вам нужно объединить большое количество строк, используя только <code>string a; a+=anotherstring[i]</code>, а не <code>stringbuilder</code>. существует очень большая разница в производительности между ними. есть много таких случаев, когда компилятор не может оптимизировать ваш код, потому что он не может понять, что задумано в большем объеме. но это может в значительной степени оптимизировать основные вещи для вас.</p>

<p>это гоча в vb.net. результат visual basic не будет повторно инициализировать переменную в этом примере:</p><pre><code>for i as integer = 1 to 100 dim j as integer console.writeline(j) j = i next ' output: 0 1 2 3 4... </code></pre><p>это будет печатать 0 в первый раз (переменные visual basic имеют значения по умолчанию при объявлении!), но каждый раз после этого <code>i</code>.</p><p>если вы добавите <code>= 0</code>, вы получите то, что ожидаете:</p><pre><code>for i as integer = 1 to 100 dim j as integer = 0 console.writeline(j) j = i next 'output: 0 0 0 0 0... </code></pre>

<p>я сделал простой тест:</p><pre><code>int b; for (int i = 0; i < 10; i++) { b = i; } </code></pre><p>против </p><pre><code>for (int i = 0; i < 10; i++) { int b = i; } </code></pre><p>я скомпилировал эти коды с помощью gcc - 5.2.0. а затем я разобрал main () этих двух кодов, и вот результат:</p><p>1º:</p><pre><code> 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 </code></pre><p>против</p><p>2º</p><pre><code> 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 </code></pre><p>которые точно так же, как результат. разве это не доказательство того, что два кода производят одно и то же?</p>

<p>я бы всегда использовал a (а не полагался на компилятор) и мог бы также переписать:</p><pre><code>for(int i=0, double intermediateresult=0; i<1000; i++){ intermediateresult = i; system.out.println(intermediateresult); } </code></pre><p>это по-прежнему ограничивает <code>intermediateresult</code> областью действия цикла, но не повторяет объявление во время каждой итерации.</p>

<p>на мой взгляд, b - лучшая структура. в последнем значении middleresult остается после завершения цикла.</p><p>правка: это не имеет большого значения для типов значений, но ссылочные типы могут быть довольно весомыми. лично мне нравится, что переменные разыменовываются как можно скорее для очистки, и b делает это для вас,</p>

<p>ну, вы всегда можете сделать это для:</p><pre><code>{ //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); } } </code></pre><p>таким образом, вы объявляете переменную только один раз, и она умрет, когда вы выйдете из цикла.</p>

<p>существует разница в c #, если вы используете переменную в лямбде и т.д. но в целом компилятор будет делать то же самое, предполагая, что переменная используется только внутри цикла. </p><p>учитывая, что они в основном одинаковы: обратите внимание, что версия b делает читателей гораздо более очевидным, что переменная не используется и не может использоваться после цикла. кроме того,<strong> версия b гораздо легче реорганизована. в версии a сложнее извлечь тело цикла в его собственный метод. </strong>более того, версия b уверяет вас, что такой рефакторинг не имеет побочных эффектов.</p><p>следовательно, версия a бесит меня бесконечно, потому что в этом нет никакой пользы, и это значительно усложняет рассуждения о коде ...</p>

<p>сотрудник предпочитает первую форму, говоря, что это оптимизация, предпочитая повторно использовать декларацию.</p><p>я предпочитаю второй (и пытаюсь убедить моего сотрудника! ;-)), прочитав это:</p><ul> <li>это уменьшает область видимости переменных там, где они нужны, и это хорошо.</li> <li>java оптимизирует достаточно, чтобы не иметь существенного различия в производительности. iirc, возможно, вторая форма еще быстрее.</li> </ul><p>в любом случае, это относится к категории преждевременной оптимизации, которая зависит от качества компилятора и/или jvm.</p>

<p>я подозреваю, что несколько компиляторов могут оптимизировать оба кода, чтобы они были одним и тем же кодом, но, конечно, не всем. так что я бы сказал, что вам лучше с первым. единственная причина последнего - если вы хотите убедиться, что объявленная переменная используется<strong> только </strong>в вашем цикле.</p>

<p>как правило, я объявляю свои переменные в самой внутренней области видимости. итак, если вы не используете промежуточный результат вне цикла, то я бы пошел с b.</p>

<p>я всегда думал, что если вы объявляете свои переменные внутри цикла, то вы тратите впустую память. если у вас есть что-то вроде этого:</p><pre><code>for(;;) { object o = new object(); } </code></pre><p>тогда не только нужно создавать объект для каждой итерации, но и для каждого объекта должна быть назначена новая ссылка. кажется, что если сборщик мусора работает медленно, то у вас будет куча свисающих ссылок, которые необходимо очистить.</p><p>однако, если у вас есть это:</p><pre><code>object o; for(;;) { o = new object(); } </code></pre><p>затем вы создаете только одну ссылку и каждый раз назначаете ей новый объект. конечно, это может занять немного больше времени, чтобы выйти из области видимости, но тогда есть только одна свисающая ссылка, чтобы иметь дело с.</p>

<p>моя практика следующая: </p><ul> <li><p>если тип переменной простой <em>(int, double, ...)</em> я предпочитаю вариант<strong> b </strong>(inside).<br> <em>причина:</em> уменьшение области видимости переменной. </p></li> <li><p>если тип переменной не простой <em>(своего рода <code>class</code> или <code>struct</code>)</em> я предпочитаю вариант<strong> a </strong>(снаружи).<br> <em>причина:</em> сокращение количества вызовов ctor-dtor.</p></li> </ul>

<p>я думаю, что это зависит от компилятора и трудно дать общий ответ.</p>

<p>с точки зрения производительности, снаружи (намного) лучше.</p><pre><code>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; } } </code></pre><p>я выполнил обе функции по 1 млрд раз каждая. outside () заняло 65 миллисекунд. inside () заняло 1,5 секунды.</p>

<p>это интересный вопрос. исходя из моего опыта, при обсуждении этого вопроса для кода возникает главный вопрос:</p><p><strong> есть ли причина, почему переменная должна быть глобальной? </strong></p><p>имеет смысл объявлять переменную только один раз, глобально, а не много раз локально, потому что это лучше для организации кода и требует меньше строк кода. однако, если его нужно объявить только локально в одном методе, я бы инициализировал его в этом методе, поэтому ясно, что переменная относится исключительно к этому методу. будьте осторожны, чтобы не вызывать эту переменную вне метода, в котором она инициализируется, если вы выберете последний вариант - ваш код не будет знать, о чем вы говорите, и сообщит об ошибке.</p><p>также, как примечание, не дублируйте имена локальных переменных между различными методами, даже если их цели почти идентичны; это просто сбивает с толку. </p>

<p>попробовал то же самое в go и сравнил вывод компилятора, используя <code>go tool compile -s</code> с go 1.9.4</p><p>нулевая разница согласно выходу ассемблера.</p>

<p>у меня был тот же самый вопрос в течение долгого времени. поэтому я протестировал еще более простой кусок кода. </p><p><strong>вывод:</strong> для <strong>таких случаев</strong> существует <strong>нет</strong> разницы в производительности.</p><p><strong>случай с внешним контуром</strong></p><pre><code>int intermediateresult; for(int i=0; i < 1000; i++){ intermediateresult = i+2; system.out.println(intermediateresult); } </code></pre><p><strong>корпус внутреннего цикла</strong></p><pre><code>for(int i=0; i < 1000; i++){ int intermediateresult = i+2; system.out.println(intermediateresult); } </code></pre><p>я проверил скомпилированный файл на декомпиляторе intellij и в обоих случаях я получил <strong>same</strong> <code>test.class</code></p><pre><code>for(int i = 0; i < 1000; ++i) { int intermediateresult = i + 2; system.out.println(intermediateresult); } </code></pre><p>я также разобрал код для обоих случаев, используя метод, приведенный в этом <a href="https://stackoverflow.com/a/30928846/7588896">answer</a> . я покажу только те части, которые имеют отношение к ответу</p><p><strong>случай с внешним контуром</strong></p><pre><code>code: stack=2, locals=3, args\_size=1 0: iconst\_0 1: istore\_2 2: iload\_2 3: sipush 1000 6: if\_icmpge 26 9: iload\_2 10: iconst\_2 11: iadd 12: istore\_1 13: getstatic #2 // field java/lang/system.out:ljava/io/printstream; 16: iload\_1 17: invokevirtual #3 // method java/io/printstream.println:(i)v 20: iinc 2, 1 23: goto 2 26: return localvariabletable: start length slot name signature 13 13 1 intermediateresult i 2 24 2 i i 0 27 0 args [ljava/lang/string; </code></pre><p><strong>корпус внутреннего цикла</strong></p><pre><code>code: stack=2, locals=3, args\_size=1 0: iconst\_0 1: istore\_1 2: iload\_1 3: sipush 1000 6: if\_icmpge 26 9: iload\_1 10: iconst\_2 11: iadd 12: istore\_2 13: getstatic #2 // field java/lang/system.out:ljava/io/printstream; 16: iload\_2 17: invokevirtual #3 // method java/io/printstream.println:(i)v 20: iinc 1, 1 23: goto 2 26: return localvariabletable: start length slot name signature 13 7 2 intermediateresult i 2 24 1 i i 0 27 0 args [ljava/lang/string; </code></pre><p>если вы обратите пристальное внимание, то только <code>slot</code>, назначенный <code>i</code> и <code>intermediateresult</code> в <code>localvariabletable</code>, будет заменен как результат их порядка появления. такая же разница в слоте отражена в других строках кода.</p><ul> <li>никаких дополнительных операций не выполняется</li> <li><code>intermediateresult</code> по-прежнему является локальной переменной в обоих случаях, поэтому нет разницы во времени доступа.</li> </ul><p><strong>бонус</strong> </p><p>компиляторы делают тонну оптимизации, взгляните на то, что происходит в этом случае.</p><p><strong>нулевой кейс</strong></p><pre><code>for(int i=0; i < 1000; i++){ int intermediateresult = i; system.out.println(intermediateresult); } </code></pre><p><strong>ноль работы декомпилированы</strong></p><pre><code>for(int i = 0; i < 1000; ++i) { system.out.println(i); } </code></pre>

<p>это лучшая форма</p><pre><code>double intermediateresult; int i = byte.minvalue; for(; i < 1000; i++) { intermediateresult = i; system.out.println(intermediateresult); } </code></pre><p>1) таким образом, один раз объявили обе переменные, а не каждую для цикла . 2) назначение стало более толстым, чем все остальные опции . 3) таким образом, правило bestpractice - это любое объявление вне итерации для.</p>

<p>a) безопаснее, чем b) ......... представьте себе, если вы инициализируете структуру в цикле, а не в int или float, что тогда?</p><p>лайк </p><pre><code>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? } </code></pre><p>вы наверняка столкнетесь с проблемами утечки памяти! поэтому я считаю, что «а» безопаснее, а «в» уязвим для накопления памяти, особенно при работе с библиотеками с закрытыми исходными кодами. вы можете проверить использование инструмента «valgrind» в linux, в частности, вспомогательного инструмента «helgrind». </p>

<p>я тестировал на js с node 4.0.0, если кому-то интересно. объявление вне цикла привело к улучшению производительности примерно на 0,5 мс в среднем за 1000 испытаний со 100 миллионами итераций цикла на испытание. так что я собираюсь сказать «вперед» и написать это наиболее читаемым/поддерживаемым способом, который b, imo. я бы поместил свой код в скрипку, но я использовал модуль node для повышения производительности. вот код:</p><pre><code>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) </code></pre>