Я всегда задавался вопросом, имеет ли вообще объявление отбрасываемой переменной перед циклом, в отличие от многократного использования внутри цикла, какую-либо разницу (производительность)? Пример (совершенно бессмысленно) на Java:
Объявление а) перед циклом:
double intermediateResult;
for(int i=0; i < 1000; i++){
intermediateResult = i;
System.out.println(intermediateResult);
}
Объявление б) (многократно) внутри цикла:
for(int i=0; i < 1000; i++){
double intermediateResult = i;
System.out.println(intermediateResult);
}
Что лучше, а или б?
Я подозреваю, что повторное объявление переменной (пример б) создает дополнительные накладные расходы теоретически, но эти компиляторы достаточно умны, так что это не имеет значения. Пример б имеет то преимущество, что он более компактен и ограничивает область действия переменной тем, где она используется. Тем не менее, я предпочитаю кодировать в соответствии с примером а.
Редактировать: Меня особенно интересует случай Java.
@AaronCarson Не могли бы вы предоставить ссылку на это предложение Google




Я думаю, это зависит от компилятора, и сложно дать общий ответ.
Это зависит от языка - IIRC C# оптимизирует это, поэтому нет никакой разницы, но JavaScript (например) будет выполнять все операции выделения памяти каждый раз.
Да, но это не так уж много. Я провел простой тест с выполнением цикла for 100 миллионов раз и обнаружил, что наибольшая разница в пользу объявления вне цикла составила 8 мс. Обычно это было больше 3-4, а иногда объявление вне цикла выполнялось ХУЖЕ (до 4 мс), но это не было типично.
Даже если я знаю, что мой компилятор достаточно умен, я не буду полагаться на него и буду использовать вариант а).
Вариант б) имеет смысл для меня только в том случае, если вам отчаянно нужно сделать промежуточный результат недоступным после тела цикла. Но я все равно не могу представить себе такую безвыходную ситуацию ....
Обновлено: Джон Скит сделал очень хорошее замечание, показав, что объявление переменной внутри цикла может иметь реальную семантическую разницу.
Я подозреваю, что несколько компиляторов могут оптимизировать оба кода, чтобы они были одним и тем же кодом, но, конечно, не все. Так что я бы сказал, что тебе лучше с первым. Единственная причина для последнего - если вы хотите убедиться, что объявленная переменная используется Только в вашем цикле.
Как правило, я объявляю свои переменные в самой внутренней возможной области. Итак, если вы не используете промежуточный результат вне цикла, я бы выбрал 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.
в примере B (исходный вопрос), действительно ли он каждый раз создает новую переменную? что творится на глазах у стека?
@Jon, это была ошибка в C# 1.0? Разве в идеале Outer не должен быть 9?
@nawfal: Я не понимаю, о чем ты. Лямбда-выражений не было в 1.0 ... и Outer является 9. Какую ошибку вы имеете в виду?
@nawfal: Я хочу сказать, что в C# 1.0 не было никаких языковых функций, в которых можно было бы отличить объявление переменной внутри цикла от объявления ее снаружи (при условии, что обе скомпилированы). Это изменилось в C# 2.0. Никакой ошибки.
@JonSkeet О да, теперь я понял, я полностью упустил из виду тот факт, что вы не можете закрывать такие переменные в 1.0, моя беда! :)
На мой взгляд, структура b лучше. В a последнее значение intermediateResult остается после завершения цикла.
Редактировать: Это не имеет большого значения для типов значений, но ссылочные типы могут быть несколько весомыми. Лично мне нравится разыменование переменных как можно скорее для очистки, и b делает это за вас,
sticks around after your loop is finished - хотя это не имеет значения для такого языка, как Python, где связанные имена остаются до завершения функции.
@ new123456: OP запросил особенности Java, даже если вопрос было задан в некотором роде. Многие языки, производные от C, имеют область видимости на уровне блоков: C, C++, Perl (с ключевым словом my), C# и Java, чтобы назвать 5, которые я использовал.
Я знаю - это было наблюдение, а не критика.
Что лучше, а или б?
С точки зрения производительности вам придется ее измерить. (И, на мой взгляд, если вы можете измерить разницу, компилятор не очень хорош).
С точки зрения обслуживания лучше использовать б. Объявляйте и инициализируйте переменные в одном месте в максимально узкой области. Не оставляйте пустых промежутков между объявлением и инициализацией и не загрязняйте пространства имен, которые вам не нужны.
Вместо Double, если он имеет дело со String, все же лучше случай "b"?
@Antoops - да, b лучше по причинам, не имеющим ничего общего с типом данных объявляемой переменной. Почему было бы иначе для струнных?
Я бы всегда использовал A (вместо того, чтобы полагаться на компилятор), а также мог бы переписать так:
for(int i=0, double intermediateResult=0; i<1000; i++){
intermediateResult = i;
System.out.println(intermediateResult);
}
Это по-прежнему ограничивает intermediateResult областью цикла, но не объявляется повторно во время каждой итерации.
Вы концептуально хотите, чтобы переменная существовала в течение всего цикла, а не отдельно для каждой итерации? Я редко это делаю. Напишите код, который максимально ясно раскрывает ваше намерение, если только у вас нет очень и очень веской причины поступить иначе.
Ах, хороший компромисс, я никогда не думал об этом! ИМО, код действительно стал немного менее визуально `` понятным '')
@Jon - я понятия не имею, что OP на самом деле делает с промежуточным значением. Просто подумал, что это стоит рассмотреть вариант.
Это ошибка 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...
Я много лет использую VB.NET и не сталкивался с этим !!
Да, на практике в этом разбираться неприятно.
Вот ссылка на это от Пола Вика: Panopticoncentral.net/archive/2006/03/28/11552.aspx
@eschneider @ferventcoder К сожалению, @PaulV решил использовать отказаться от его старых сообщений в блоге, так что теперь это мертвая ссылка.
да, совсем недавно с этим сталкивался; искал официальные документы по этому поводу ...
да, он не затемняет переменную снова. столкнулся с этим вчера, и ряд поисковиков позволил этому.
Что ж, я запускал ваши примеры A и B по 20 раз, зацикливался 100 миллионов раз (JVM - 1.5.0).
A: среднее время выполнения: 0,074 секунды
B: среднее время выполнения: 0,067 сек.
К моему удивлению, B оказался немного быстрее. Какими бы быстрыми ни были компьютеры, трудно сказать, можно ли это точно измерить. Я бы тоже запрограммировал его на A, но я бы сказал, что это не имеет особого значения.
Ты победил меня. Я как раз собирался опубликовать свои результаты для профилирования, я получил более или менее то же самое, и да, на удивление, B быстрее, я бы подумал, что A, если бы мне нужно было сделать на это ставку.
Хорошо, круто, да, я смотрел только на время выполнения, как указал Р. Бемроуз в A, переменная остается после завершения цикла. Результаты вашего профиля рассказали вам что-нибудь об использовании памяти?
Не удивительно - когда переменная является локальной для цикла, ее не нужно сохранять после каждой итерации, поэтому она может оставаться в регистре.
+1 за на самом деле тестирую это, а не просто мнение / теорию, которую ОП мог придумать сам.
Какое время выполнения после запуска JIT?
@GoodPerson, честно говоря, я бы хотел, чтобы это было сделано. Я запускал этот тест примерно 10 раз на своей машине для 50 000 000–100 000 000 итераций с почти идентичным фрагментом кода (которым я хотел бы поделиться со всеми, кто хочет запускать статистику). Ответы были разделены почти поровну в обе стороны, обычно с разницей в 900 мс (более 50 миллионов итераций), что на самом деле немного. Хотя моя первая мысль заключается в том, что это будет «шум», он может наклоняться один за другим. Мне эти усилия кажутся чисто академическими (для большинства реальных приложений) .. Я бы все равно хотел увидеть результат;) Кто-нибудь согласен?
@Wolf Ты не понимаешь, я не говорю, что один лучше другого. Я просто согласен с Рабарберски в том, что я также стараюсь кодировать пример «А».
Хорошо, тогда извините за голос против. Как вы думаете, можно ли улучшить свой ответ (мой голос сейчас заблокирован)? Кстати: на самом деле измерение, а не предположение, стоит поставить +1. Но с точки зрения производительности разработчика, я бы предпочел использовать версию B, если нет свидетельств снижения производительности (обычно тела цикла не такие тривиальные).
В этом нет никакого смысла. Было бы лучше, если бы вы предоставили статистику. Почему, черт возьми, А должно быть медленнее, чем В !?
Ставка на мой лучший фунт стерлингов получила 99% голосов без проверки.
Если вы не использовали хорошую систему микропрофилирования, есть множество причин, по которым A работает лучше, чем B, которые не связаны с различиями в коде. Вы пробовали запустить тесты в обратном порядке? Вы запускали циклы разогрева перед запуском тестов по времени? Как вы справлялись со временем ввода-вывода и учитывали ли вы его собственную (иногда весьма существенную) изменчивость?
Показывать результаты тестов без документирования настроек бесполезно. Это особенно верно в этом случае, когда оба фрагмента кода создают идентичный байт-код, поэтому любая измеренная разница является лишь признаком недостаточных условий тестирования.
@TedHopp. В вашем комментарии говорится, что A работает лучше, чем B, тогда как на самом деле B работает лучше, чем A.
@CollinBell - Да, я ошибся. Тем не менее, это не имеет значения для того, что я пытался сделать в своем комментарии.
Сотрудник предпочитает первую форму, говоря, что это оптимизация, и предпочитает повторно использовать объявление.
Я предпочитаю второй (и пытаюсь уговорить коллегу! ;-)), прочитав это:
В любом случае, он попадает в категорию преждевременной оптимизации, которая зависит от качества компилятора и / или JVM.
Ниже приводится то, что я написал и скомпилировал в .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 / байт-код, а во время выполнения преобразуется в машинный язык. Таким образом, на машинном языке дубль даже не может быть создан в стеке. Это может быть просто регистр, поскольку код отражает, что это временная переменная для функции WriteLine. Только для циклов существует целый набор правил оптимизации. Так что обычному парню не стоит беспокоиться об этом, особенно если речь идет о управляемых языках. Есть случаи, когда вы можете оптимизировать управляющий код, например, если вам нужно объединить большое количество строк, используя только string a; a+=anotherstring[i], а не используя StringBuilder. Между ними очень большая разница в производительности. Есть много таких случаев, когда компилятор не может оптимизировать ваш код, потому что он не может понять, что предполагается в более широком масштабе. Но он может в значительной степени оптимизировать для вас базовые вещи.
int j = 0 для (; j <0x3e8; j ++) таким образом объявляют один раз одновременно обе переменные, а не каждую для цикла. 2) назначение жирнее всех остальных вариантов. 3) Таким образом, лучшим правилом практики является любое объявление вне итерации для.
Я всегда думал, что если вы объявляете свои переменные внутри цикла, вы тратите память. Если у вас что-то вроде этого:
for(;;) {
Object o = new Object();
}
Тогда не только объект должен быть создан для каждой итерации, но и должна быть выделена новая ссылка для каждого объекта. Кажется, что если сборщик мусора работает медленно, у вас будет куча висящих ссылок, которые необходимо очистить.
Однако, если у вас есть это:
Object o;
for(;;) {
o = new Object();
}
Тогда вы создаете только одну ссылку и каждый раз назначаете ей новый объект. Конечно, это может занять немного больше времени, чтобы он вышел за пределы области видимости, но тогда нужно иметь дело только с одной висящей ссылкой.
Хотя я не могу говорить о Java, в .NET ссылка не «выделяется» для каждого объекта в первом примере. В стеке есть единственная запись для этой локальной (для метода) переменной. Для ваших примеров созданный IL идентичен.
Новая ссылка не выделяется для каждого объекта, даже если ссылка объявлена в цикле for. В ОБЕИХ случаях: 1) 'o' - это локальная переменная, и пространство стека выделяется для нее один раз в начале функции. 2) На каждой итерации создается новый объект. Так что разницы в производительности нет. Для организации кода, удобочитаемости и ремонтопригодности лучше объявить ссылку внутри цикла.
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» уязвима для накопления памяти, особенно при работе с библиотеками с близким исходным кодом. Вы можете проверить использование инструмента «Valgrind» в Linux, в частности вспомогательного инструмента «Helgrind».
Что ж, вы всегда можете найти место для этого:
{ //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);
}
}
Таким образом, вы объявляете переменную только один раз, и она умрет, когда вы выйдете из цикла.
В C# есть разница, если вы используете переменную в лямбде и т. д. Но в целом компилятор будет делать то же самое, предполагая, что переменная используется только внутри цикла.
Учитывая, что они в основном одинаковы: обратите внимание, что версия b делает гораздо более очевидным для читателей, что переменная не может и не может использоваться после цикла. Кроме того, версию b гораздо легче реорганизовать. В версии а сложнее извлечь тело цикла в собственный метод. Более того, версия b гарантирует отсутствие побочных эффектов такого рефакторинга.
Следовательно, версия a меня до бесконечности раздражает, потому что она бесполезна и значительно усложняет рассуждение о коде ...
Моя практика следующая:
если тип переменной простой (число, двойное, ...), я предпочитаю вариант б (внутри) .
Причина: уменьшает объем переменной.
если тип переменной не простой (какой то class или struct) Я предпочитаю вариант а (снаружи) .
Причина: уменьшает количество вызовов ctor-dtor.
Это интересный вопрос. По моему опыту, когда вы обсуждаете этот вопрос для кода, следует учитывать главный вопрос:
Есть ли причина, по которой переменная должна быть глобальной?
Имеет смысл объявлять переменную только один раз, глобально, а не много раз локально, потому что это лучше для организации кода и требует меньше строк кода. Однако, если ее нужно объявить локально только в одном методе, я бы инициализировал ее в этом методе, чтобы было ясно, что переменная имеет отношение исключительно к этому методу. Будьте осторожны, не вызывайте эту переменную вне метода, в котором она инициализируется, если вы выберете последний вариант - ваш код не будет знать, о чем вы говорите, и сообщит об ошибке.
Также, в качестве примечания, не дублируйте имена локальных переменных между разными методами, даже если их цели почти идентичны; это просто сбивает с толку.
lol Я не согласен по многим причинам ... Тем не менее, голосование против ... Я уважаю ваше право выбора
С точки зрения производительности, снаружи (намного) лучше.
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 миллиарду раз каждую. external () занял 65 миллисекунд. inside () занял 1,5 секунды.
Должно быть, тогда была неоптимизированная компиляция Debug, а?
int j = 0 для (; j <0x3e8; j ++) таким образом объявляют один раз одновременно обе переменные, а не каждую для цикла. 2) назначение жирнее всех остальных вариантов. 3) Таким образом, лучшим правилом практики является любое объявление вне итерации для.
Провел простой тест:
int b;
for (int i = 0; i < 10; i++) {
b = i;
}
против
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>: ret
против
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
Которые точно такие же, как и результат. Разве это не доказательство того, что два кода производят одно и то же?
да, и здорово, что вы это сделали, но это возвращается к тому, что люди говорили о зависимости языка / компилятора. Интересно, как это повлияет на производительность JIT или интерпретируемого языка.
Я тестировал JS с Node 4.0.0, если кому-то интересно. Объявление вне цикла привело к повышению производительности примерно на 0,5 мс в среднем за 1000 испытаний со 100 миллионами итераций цикла за испытание. Так что я собираюсь сказать, продолжайте и напишите его наиболее читаемым / поддерживаемым способом, которым является B, imo. Я бы поместил свой код в скрипку, но я использовал модуль Node для повышения производительности. Вот код:
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.info('declared inside loop', insideAvg)
console.info('declared outside loop', outsideAvg)
это лучшая форма
double intermediateResult;
int i = byte.MinValue;
for(; i < 1000; i++)
{
intermediateResult = i;
System.out.println(intermediateResult);
}
1) таким образом однажды объявили раз и переменную, а не каждую для цикла. 2) назначение жирнее всех остальных вариантов. 3) Таким образом, лучшим правилом практики является любое объявление вне итерации для.
Попробовал то же самое в Go и сравнил вывод компилятора с использованием go tool compile -S с go 1.9.4
Нулевая разница, согласно выводам ассемблера.
У меня долгое время был один и тот же вопрос. Поэтому я протестировал еще более простой фрагмент кода.
Заключение: Для такие случаи существует разница в производительности НЕТ.
Внешний корпус петли
int intermediateResult;
for(int i=0; i < 1000; i++){
intermediateResult = i+2;
System.out.println(intermediateResult);
}
Внутренний корпус петли
for(int i=0; i < 1000; i++){
int intermediateResult = i+2;
System.out.println(intermediateResult);
}
Я проверил скомпилированный файл в декомпиляторе IntelliJ и в обоих случаях получил одно и тожеTest.class
for(int i = 0; i < 1000; ++i) {
int intermediateResult = i + 2;
System.out.println(intermediateResult);
}
Я также разобрал код для обоих случаев, используя метод, указанный в этом отвечать. Я покажу только те части, которые имеют отношение к ответу
Внешний корпус петли
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:
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;
Если вы обратите пристальное внимание, только Slot, назначенные для i и intermediateResult в LocalVariableTable, меняются местами в зависимости от порядка их появления. Такая же разница в слоте отражается в других строках кода.
intermediateResult по-прежнему является локальной переменной в обоих случаях, поэтому нет разницы во времени доступа.БОНУС
Компиляторы делают массу оптимизаций, посмотрите, что в этом случае происходит.
Нулевой рабочий случай
for(int i=0; i < 1000; i++){
int intermediateResult = i;
System.out.println(intermediateResult);
}
Ноль работы декомпилирован
for(int i = 0; i < 1000; ++i) {
System.out.println(i);
}
Это важно при написании кода Java для платформы Android. Google предлагает, чтобы критический по времени код объявлял увеличивающиеся переменные вне цикла for, как если бы внутри цикла for, он повторно объявлял это каждый раз в этой среде. Разница в производительности очень заметна для дорогих алгоритмов.