Есть ли ограничение на количество вложенных циклов for?

79

Поскольку у всего есть предел, мне было интересно, есть ли ограничение на количество вложенных forциклов или пока у меня есть память, я могу их добавить, может ли компилятор Visual Studio создать такую ​​программу?

Конечно, 64 и более вложенных forциклов не подходят для отладки, но выполнимо ли это?

private void TestForLoop()
{
  for (int a = 0; a < 4; a++)
  {
    for (int b = 0; b < 56; b++)
    {
      for (int c = 0; c < 196; c++)
      {
        //etc....
      }
    }
  }
}
Фред Смит
источник
78
Вы программист: ответьте на свой вопрос с помощью компьютерного программирования. Напишите программу, которая генерирует, компилирует и запускает программы с 1, 2, 4, 8, 16, 32, ... вложенными циклами, и вы очень быстро узнаете, есть ли предел.
Эрик Липперт
38
№1. OP не указал, что это каким-либо образом связано с производственным кодом, а не только с гипотетическим вопросом типа «что, если». №2. Независимо от того, сколько вложенных циклов делает OP во время экспериментов, они никогда не дойдут до бесконечности; независимо от того, насколько далеко OP продвигает его, единственный способ когда-либо доказать, существует ли предел, - это если и когда он действительно сломается.
Panzercrisis
10
«Поскольку все имеет предел», нет: sin (x) не имеет предела для x -> oo. Также: если ваша посылка «все ограничено», почему вы даже спрашиваете «ограничено ли это»? Ответ находится в ваших предположениях, что делает аргумент совершенно бесполезным и тривиальным.
Bakuriu
5
@EricLippert Строго говоря, вы никогда не были бы уверены, что не было предела, как бы долго вы ни продолжали этот путь.
Кейси
2
«x -> oo» заставило меня немного поплакать :) Используйте «x → ∞»
Ману

Ответы:

120

Я рискну, опубликовав это, но думаю, что ответ таков:

Между 550 и 575

с настройками по умолчанию в Visual Studio 2015

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

for (int i0=0; i0<10; i0++)
{
    for (int i1=0; i1<10; i1++)
    {
        ...
        ...
        for (int i573=0; i573<10; i573++)
        {
            for (int i574=0; i574<10; i574++)
            {
                Console.WriteLine(i574);
            }
        }
        ...
        ...
    }
}

Для 500 вложенных циклов программа все еще может быть скомпилирована. С 575 циклами компилятор выручает:

Предупреждение Анализатор AD0001 «Microsoft.CodeAnalysis.CSharp.Diagnostics.SimplifyTypeNames.CSharpSimplifyTypeNamesDiagnosticAnalyzer» выдал исключение типа «System.InsufficientExecutionStackException» с сообщением «Недостаточно стека для продолжения безопасного выполнения программы. Это может произойти из-за того, что слишком много функций в стеке вызовов или функция в стеке использует слишком много места в стеке. '.

с базовым сообщением компилятора

ошибка CS8078: выражение слишком длинное или сложное для компиляции

Конечно, это чисто гипотетический результат. Если самый внутренний цикл выполняет больше, чем a Console.WriteLine, то может быть возможно меньше вложенных циклов до того, как размер стека будет превышен. Кроме того, это не может быть строго техническим ограничением в том смысле, что могут быть скрытые настройки для увеличения максимального размера стека для «Анализатора», упомянутого в сообщении об ошибке, или (при необходимости) для результирующего исполняемого файла. Однако эта часть ответа остается на усмотрение людей, глубоко знающих C #.


Обновить

В ответ на вопрос в комментариях :

Мне было бы интересно увидеть, как этот ответ будет расширен, чтобы «экспериментально доказать», можно ли поместить 575 локальных переменных в стек, если они не используются в циклах for, и / или можно ли поместить 575 не вложенных циклов for в единственная функция

В обоих случаях ответ: да, это возможно. При заполнении метода 575 автоматически сгенерированными операторами

int i0=0;
Console.WriteLine(i0);
int i1=0;
Console.WriteLine(i1);
...
int i574=0;
Console.WriteLine(i574);

его еще можно скомпилировать. Все остальное меня бы удивило. Размер стека, который требуется для intпеременных, составляет всего 2,3 КБ. Но мне было любопытно, и я увеличил это число, чтобы проверить дальнейшие пределы. В конце концов, он не скомпилировался, что привело к ошибке.

ошибка CS0204: разрешены только 65534 локальных, включая сгенерированные компилятором

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

Аналогичным образом , 575 невложенных for -loops, как и в

for (int i0=0; i0<10; i0++)
{
    Console.WriteLine(i0);
}
for (int i1=0; i1<10; i1++)
{
    Console.WriteLine(i1);
}
...
for (int i574=0; i574<10; i574++)
{
    Console.WriteLine(i574);
}

также могут быть скомпилированы. Здесь я также попытался найти предел и создал больше таких циклов. В частности, я не был уверен, считаются ли переменные цикла в этом случае также «локальными», потому что они сами по себе { block }. Но все равно больше 65534 невозможно. Напоследок добавил тест, состоящий из 40000 петель узора

for (int i39999 = 0; i39999 < 10; i39999++)
{
    int j = 0;
    Console.WriteLine(j + i39999);
}

которые содержат дополнительную переменную в цикле, но они, кажется, тоже считаются «локальными», и это было невозможно скомпилировать.


Итак, подведем итог: ограничение ~ 550 действительно вызвано глубиной вложенности петель. На это также указывает сообщение об ошибке

ошибка CS8078: выражение слишком длинное или сложное для компиляции

К сожалению, в документации по ошибке CS1647 (но по понятным причинам) не указывается «измерение» сложности, а дается только практический совет.

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

Еще раз подчеркнем: для частного случая глубоко вложенных forциклов все это скорее академическое и гипотетическое . Но веб-поиск сообщения об ошибке CS1647 показывает несколько случаев, когда эта ошибка появлялась для кода, который, скорее всего, не был намеренно сложным, а был создан в реалистичных сценариях.

Marco13
источник
2
@PatrickHofman Да, намек «с настройками по умолчанию ...» важен: это относится к наивному созданию проекта по умолчанию в VS2015. В настройках проекта не отображался "очевидный" параметр увеличения лимита. Но независимо от этого, мне интересно, нет ли каких-либо дополнительных ограничений, налагаемых CLI / CLR / чем-то еще. В спецификации ECMA-335 есть раздел «III.1.7.4 Должен предоставлять maxstack» , но я слишком большой новичок в C #, чтобы сказать, действительно ли это связано, или даже подробно проанализировать его значение ...
Marco13,
10
Microsoft не впервые установит ограничение на количество forциклов. У Microsoft BASIC был лимит вложенности 8.
Марк,
3
@Davislor: сообщение об ошибке относится к размеру стека в компиляторе , который также является программой и рекурсивно обрабатывает конструкцию с вложенным циклом. Это (сильно) не привязано к количеству локальных переменных в коде при компиляции.
ruakh
2
Это только я? Я нахожу этот ответ очень смешным! Я бы тоже подумал 42как ответ.
cmroanirgo
11
Вероятно, стоит отметить, что выполнение 500 вложенных циклов, предполагающих только 2 итерации на цикл и одну итерацию на цикл, займет около 10 ^ 33 гугол лет (10 ^ 133) на компьютере с частотой 4 ГГц. Для сравнения возраст Вселенной составляет примерно 1,37 x 10 ^ 10 лет. Учитывая, что по прогнозам нуклоны распадутся примерно через 10-40 лет, я могу прямо сказать, что современное оборудование не рассчитано на такой долгий срок службы, и вам может потребоваться перезагрузить компьютер тем временем.
Maciej Piechotka
40

В спецификации языка C # или в среде CLR нет жесткого ограничения. Ваш код будет итеративным, а не рекурсивным, что может довольно быстро привести к переполнению стека.

Есть несколько вещей, которые могут считаться пороговыми, например (обычно) intсчетчик, который вы использовали бы, который выделял бы intв памяти для каждого цикла (и до того, как вы распределили весь свой стек с помощью int ...). Обратите внимание, что intэто необходимо, и вы можете повторно использовать одну и ту же переменную.

Как указал Марко , текущий порог больше в компиляторе, чем в фактической спецификации языка или во время выполнения. Как только это будет перекодировано, вы можете сделать еще несколько итераций. Если вы, например, используете Ideone , который по умолчанию использует старый компилятор, вы можете forлегко получить более 1200 циклов.

Однако такое количество циклов for указывает на плохой дизайн. Надеюсь, этот вопрос чисто гипотетический.

Патрик Хофман
источник
Я согласен с этим, но добавлю, что вы, скорее всего, достигнете ограничения по времени / оборудованию, прежде чем достигнете ограничения программного обеспечения (например, компиляция занимает слишком много времени или ваш код требует слишком много времени / слишком много ресурсов для выполнения).
Marshall Tigerus
Файлы кода из миллиона строк компилируются достаточно быстро, так что это не проблема. Кроме того, поскольку код довольно прост для механизма выполнения, я не ожидал бы от него слишком многого с точки зрения производительности.
Патрик Хофман,
1
Да ладно, в самом языке нет предела. То, что файловая система занимает всего 20 МБ, а исходный файл или исполняемый файл становится слишком большим, не является настоящим ограничением @ Marco13
Патрик Хофман
3
Вопреки вашему ответу, каждый цикл фактически добавляет к используемому пространству стека. (Он добавляет новую переменную.) Следовательно, стек закончится, когда у вас будет достаточно переменных. Это точно такая же проблема, как и с рекурсией (за исключением того, что кадр стека для метода будет занимать больше места в стеке, чем один int). Если бы это были циклы без переменной цикла, все было бы иначе.
Servy
3
Я считаю, что CPython ограничивает вложенность языковых конструкций примерно 50. Нет смысла поддерживать даже 1200 вложенных циклов ... уже 10 - явный признак того, что с программой что-то не так.
Bakuriu
13

Существует предел для всего C #, скомпилированного до MSIL. MSIL может поддерживать только 65535 локальных переменных. Если ваши forциклы похожи на те, которые вы показали в примере, для каждого из них требуется переменная.

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

Корт Аммон
источник
6
Для цикла for не нужны никакие переменные.
Патрик Хофман,
1
@PatrickHofman Вы правы, я отредактировал ту часть, которую забыл включить
Cort Ammon
@PatrickHofman, если я не ошибаюсь, цикл for без переменных эквивалентен тому, while(true)который формирует бесконечный цикл. Если мы отредактируем вопрос на «Есть ли ограничение на количество вложенных циклов for в завершающейся программе?» тогда можем ли мы использовать трюк с локальной переменной для создания жесткого ограничения?
emory
2
@emory Ничто не останавливает вас от некоторых действительно абсурдных циклов, в которых повторно используются переменные. Я бы не назвал их значимыми для циклов, но они могут быть выполнены.
Cort Ammon
1
@emory, вы можете завершить цикл с помощью a breakили сделать так, чтобы условие цикла проверяло что-то внешнее, например( ; DateTime.now() < ... ; )
Гриша Левит
7

От 800 до 900 для пустых for(;;)петель.

Отраженный подход Marco13, кроме проверенных for(;;)циклов:

for (;;)  // 0
for (;;)  // 1
for (;;)  // 2
// ...
for (;;)  // n_max
{
  // empty body
}

Он работал для 800 вложенных for(;;)циклов, но выдавал ту же ошибку, что и Marco13 при попытке 900 циклов.

Когда он компилируется, for(;;)кажется, что поток блокируется, не перегружая ЦП; на первый взгляд кажется, что он действует как Thread.Sleep().

Нат
источник
2
«он завершает работу практически мгновенно» - это странно - я думал, что for(;;)это будет бесконечный цикл в C # ?! (Я не могу попробовать прямо сейчас, но если окажется, что это не так, я удалю этот комментарий)
Marco13,
@ Marco13: Ага, ты прав! Не уверен, что происходило в моем тестовом коде ранее, но for(;;)блокируется без использования процессорного времени.
Nat
Цикл for (;;) - это бесконечный цикл, поэтому зачем добавлять еще один цикл for (;;) или их 800, потому что они не будут выполняться. первый будет работать вечно.
Фред Смит
1
@FredSmith: for(;;)Циклы вложены, поэтому все они начинаются до самого вложенного цикла, который бесконечен. Чтобы for(;;)заблокировать первый , это должно быть for(;;){}. Что касается того, зачем это нужно, это был просто тест, чтобы увидеть, какие ограничения имеет текущая реализация .NET / C #; очевидно, он не может перейти на 900 for(;;), хотя, как вы заметили, ничего не делает.
Nat