Czy istnieje ograniczenie liczby zagnieżdżonych pętli „for”?

79

Ponieważ wszystko ma swój limit, zastanawiałem się, czy istnieje ograniczenie liczby zagnieżdżonych forpętli, czy jeśli mam pamięć, mogę je dodać, czy kompilator Visual Studio może stworzyć taki program?

Oczywiście 64 lub więcej zagnieżdżonych forpętli nie byłoby przydatne do debugowania, ale czy jest to wykonalne?

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....
      }
    }
  }
}
Fred Smith
źródło
78
Jesteś programistą: odpowiedz na swoje pytanie z programowania. Napisz program, który generuje, kompiluje i uruchamia programy z 1, 2, 4, 8, 16, 32, ... zagnieżdżonymi pętlami, a bardzo szybko dowiesz się, czy istnieje ograniczenie.
Eric Lippert
38
# 1. W PO nie wskazano, że jest to w jakikolwiek sposób związane z kodem produkcji, w przeciwieństwie do tylko hipotetycznego pytania typu „co by było, gdyby”. # 2. Bez względu na to, ile zagnieżdżonych pętli OP utworzy podczas eksperymentów, nigdy nie osiągną nieskończoności; bez względu na to, jak daleko posuwa się PO, jedynym sposobem, który kiedykolwiek udowodni, czy istnieje limit, jest to, czy i kiedy faktycznie się łamie.
Panzercrisis,
10
„Ponieważ wszystko ma swoje granice”, no: sin (x) nie ma limitu dla x -> oo. Ponadto: jeśli twoim założeniem jest „wszystko jest ograniczone”, dlaczego w ogóle pytasz „czy to jest ograniczone”? Odpowiedź tkwi w twoich założeniach, co sprawia, że ​​argument jest całkowicie bezużyteczny i trywialny.
Bakuriu,
5
@EricLippert Ściśle mówiąc, nigdy nie byłbyś pewien, czy nie ma ograniczeń, bez względu na to, jak długo kontynuowałeś tę drogę.
Casey
2
„x -> oo” trochę mnie płakało :) Użyj „x → ∞”
Manu,

Odpowiedzi:

120

Wychodzę na skraj, publikując to, ale myślę, że odpowiedź brzmi:

Między 550 a 575

z ustawieniami domyślnymi w programie Visual Studio 2015

Stworzyłem mały program, który generuje zagnieżdżone forpętle ...

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);
            }
        }
        ...
        ...
    }
}

W przypadku 500 zagnieżdżonych pętli program można nadal skompilować. W przypadku pętli 575 kompilator wyskakuje:

Ostrzeżenie Analizator AD0001 „Microsoft.CodeAnalysis.CSharp.Diagnostics.SimplifyTypeNames.CSharpSimplifyTypeNamesDiagnosticAnalyzer” zgłosił wyjątek typu „System.InsufficientExecutionStackException” z komunikatem „Niewystarczający stos, aby kontynuować bezpieczne wykonywanie programu. Może się tak zdarzyć, gdy na stosie wywołań znajduje się zbyt wiele funkcji lub funkcja na stosie zajmuje zbyt dużo miejsca. '.

z podstawowym komunikatem kompilatora

błąd CS8078: Wyrażenie jest zbyt długie lub złożone, aby można było je skompilować

Oczywiście jest to czysto hipotetyczny wynik. Jeśli najbardziej wewnętrzna pętla robi więcej niż a Console.WriteLine, to mniej zagnieżdżonych pętli może być możliwych, zanim rozmiar stosu zostanie przekroczony. Może to również nie być ściśle techniczne ograniczenie w tym sensie, że mogą istnieć ukryte ustawienia zwiększające maksymalny rozmiar stosu dla „Analizatora”, o którym mowa w komunikacie o błędzie, lub (jeśli to konieczne) dla wynikowego pliku wykonywalnego. Jednak ta część odpowiedzi jest pozostawiona osobom, które dogłębnie znają język C #.


Aktualizacja

W odpowiedzi na pytanie w komentarzach :

Chciałbym zobaczyć tę odpowiedź rozwiniętą, aby "udowodnić" eksperymentalnie, czy możesz umieścić 575 zmiennych lokalnych na stosie, jeśli nie są one używane w pętlach for i / lub czy możesz umieścić 575 niezagnieżdżonych pętli for w pojedyncza funkcja

W obu przypadkach odpowiedź brzmi: tak, jest to możliwe. Podczas wypełniania metody 575 automatycznie wygenerowanymi wyciągami

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

nadal można go skompilować. Wszystko inne by mnie zaskoczyło. Rozmiar stosu wymagany dla intzmiennych to zaledwie 2,3 KB. Ale byłem zaciekawiony i aby sprawdzić dalsze ograniczenia, zwiększyłem tę liczbę. Ostatecznie nie skompilował się, powodując błąd

błąd CS0204: Dozwolone są tylko 65534 lokalizacje lokalne, w tym te wygenerowane przez kompilator

co jest interesujące, ale zostało już zaobserwowane gdzie indziej: Maksymalna liczba zmiennych w metodzie

Podobnie, 575 zagnieżdżane for -loops, jak w

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);
}

można również skompilować. Tutaj również próbowałem znaleźć limit i utworzyłem więcej tych pętli. W szczególności nie byłem pewien, czy zmienne pętli w tym przypadku również liczą się jako „lokalne”, ponieważ są one same w sobie { block }. Ale nadal więcej niż 65534 nie jest możliwe. Na koniec dodałem test składający się z 40000 pętli wzoru

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

który zawierał dodatkową zmienną w pętli, ale wydaje się, że są one również liczone jako „lokalne” i nie można było tego skompilować.


Podsumowując: granica ~ 550 jest rzeczywiście spowodowana głębokością zagnieżdżenia pętli. Wskazuje na to również komunikat o błędzie

błąd CS8078: Wyrażenie jest zbyt długie lub złożone, aby można było je skompilować

Dokumentacja błędów CS1647 niestety (ale ze zrozumiałych względów) nie określa „pomiar” złożoności, ale tylko daje pragmatyczne porady

W kompilatorze przetwarzającym kod wystąpiło przepełnienie stosu. Aby rozwiązać ten błąd, uprość kod.

Aby jeszcze raz to podkreślić: w szczególnym przypadku głęboko zagnieżdżonych forpętli wszystko to jest raczej akademickie i hipotetyczne . Jednak wyszukiwanie w Internecie komunikatu o błędzie CS1647 ujawnia kilka przypadków, w których ten błąd pojawił się dla kodu, który najprawdopodobniej nie był celowo złożony, ale został utworzony w realistycznych scenariuszach.

Marco13
źródło
2
@PatrickHofman Tak, podpowiedź „z ustawieniami domyślnymi ...” jest ważna: odnosi się to naiwnie do tworzenia domyślnego projektu w VS2015. Ustawienia projektu nie pokazywały „oczywistego” parametru zwiększającego limit. Ale niezależnie od tego, zastanawiam się, czy nie ma żadnych dalszych ograniczeń narzuconych przez CLI / CLR / cokolwiek. Specyfikacja ECMA-335 ma sekcję „III.1.7.4 Musi zapewnić maxstack” , ale jestem zbyt wielkim noobem C #, aby powiedzieć, czy jest to naprawdę powiązane, czy nawet szczegółowo przeanalizować jego znaczenie ...
Marco13
10
Nie byłby to pierwszy raz, gdy Microsoft miałby ograniczenie forpętli. Microsoft BASIC miał limit zagnieżdżenia wynoszący 8.
Mark
3
@Davislor: Komunikat o błędzie odnosi się do rozmiaru stosu w kompilatorze , który jest również programem i który rekurencyjnie przetwarza konstrukcję zapętloną zagnieżdżoną. Nie jest (silnie) związana z liczbą zmiennych lokalnych w kompilowanym kodzie.
ruakh
2
Czy to tylko ja? Uważam, że ta odpowiedź jest całkowicie zabawna! Pomyślałbym również, że 42to odpowiedź.
cmroanirgo
11
Prawdopodobnie warto zauważyć, że 500 zagnieżdżonych pętli przy założeniu tylko 2 iteracji na pętlę i jednej iteracji na cykl zajmie około 10 ^ 33 googol lat (10 ^ 133), aby uruchomić na komputerze 4 GHz. Dla porównania wiek wszechświata to około 1,37 x 10 ^ 10 lat. Biorąc pod uwagę, że przewiduje się, że nukleony rozpadną się za około 10 ^ 40 lat, mogę z czystym sumieniem powiedzieć, że obecny sprzęt nie jest zbudowany tak długo, że w międzyczasie może być konieczne ponowne uruchomienie komputera.
Maciej Piechotka
40

Nie ma sztywnego limitu w specyfikacji języka C # ani w środowisku CLR. Twój kod byłby iteracyjny, a nie rekurencyjny, co mogłoby doprowadzić do dość szybkiego przepełnienia stosu.

Jest kilka rzeczy, które mogą być liczone jako próg, na przykład (ogólnie) intlicznik, którego używałbyś, który przydzielałby intw pamięci dla każdej pętli (i zanim przydzielisz cały stos intami ...). Pamiętaj, że użycie tego intjest wymagane i możesz ponownie użyć tej samej zmiennej.

Jak zauważył Marco , bieżący próg występuje bardziej w kompilatorze niż w rzeczywistej specyfikacji języka lub w czasie wykonywania. Po przekodowaniu możesz uzyskać kilka kolejnych iteracji. Jeśli na przykład używasz Ideone , który domyślnie używa starego kompilatora, możesz łatwo uzyskać ponad 1200 forpętli.

Tyle w przypadku pętli wskazuje jednak na zły projekt. Mam nadzieję, że to pytanie jest czysto hipotetyczne.

Patrick Hofman
źródło
Zgadzam się z tym, ale dodałbym, że prawdopodobnie osiągniesz limit czasu / sprzętu, zanim osiągniesz ograniczenie oprogramowania (np. Kompilacja trwa zbyt długo lub Twój kod zajmuje zbyt dużo czasu / zbyt wiele zasobów do wykonania).
Marshall Tigerus
Pliki kodów zawierające milion wierszy kompilują się dość szybko, więc nie byłoby problemu. Ponadto, ponieważ kod jest dość prosty dla silnika wykonawczego, nie oczekiwałbym od niego zbyt wiele pod względem wydajności.
Patrick Hofman,
1
Chodź, sam język nie ma ograniczeń. To, że system plików ma tylko 20 MB, a plik źródłowy lub plik wykonywalny staje się zbyt duży, nie jest prawdziwym ograniczeniem @ Marco13
Patrick Hofman
3
W przeciwieństwie do twojej odpowiedzi, każda pętla w rzeczywistości zwiększa wykorzystaną przestrzeń stosu. (Dodaje nową zmienną.) Dlatego stos się wyczerpie, gdy będzie wystarczająco dużo zmiennych. Jest to dokładnie ten sam problem, co w przypadku rekurencji (z wyjątkiem tego, że ramka stosu metody będzie zajmować więcej miejsca na stosie niż tylko jedna int). Gdyby były to pętle bez zmiennej pętli, byłoby inaczej.
Servy
3
Uważam, że CPython ogranicza zagnieżdżanie konstrukcji językowych do około 50. Nie ma sensu wspierać nawet 1200 zagnieżdżonych pętli ... już 10 to wyraźny znak, że coś jest nie tak z programem.
Bakuriu
13

Istnieje limit dla wszystkich C # skompilowanych do MSIL. MSIL może obsługiwać tylko 65535 zmiennych lokalnych. Jeśli twoje forpętle są takie jak te, które pokazałeś w przykładzie, każda z nich wymaga zmiennej.

Możliwe, że Twój kompilator może przydzielić obiekty na stercie, aby działały jako magazyn dla zmiennych lokalnych, omijając ten limit. Nie jestem jednak pewien, jakie dziwne rezultaty wynikną z tego. Podczas refleksji mogą pojawić się kwestie, które czynią takie podejście nielegalnym.

Cort Ammon
źródło
6
Nie potrzebujesz żadnych zmiennych do pętli for.
Patrick Hofman
1
@PatrickHofman Masz rację, zredagowałem w części, o której zapomniałem dodać
Cort Ammon
@PatrickHofman, jeśli się nie mylę, pętla for bez zmiennych jest odpowiednikiem tego, while(true)który utworzyłby nieskończoną pętlę. Jeśli zredagowaliśmy pytanie „Czy istnieje ograniczenie liczby zagnieżdżonych pętli for w programie kończącym?” czy moglibyśmy użyć sztuczki ze zmienną lokalną, aby utworzyć sztywny limit?
emory
2
@emory Nic Cię nie powstrzyma przed naprawdę absurdalnymi pętlami, które ponownie wykorzystują zmienne. Nie nazwałbym ich znaczącymi dla pętli, ale można to zrobić.
Cort Ammon
1
@emory, możesz zakończyć pętlę breaklub sprawić, by warunek pętli sprawdził coś zewnętrznego, na przykład( ; DateTime.now() < ... ; )
Grisha Levit
7

Od 800 do 900 dla pustych for(;;)pętli.

Odzwierciedlono podejście Marco13, z wyjątkiem wypróbowanych for(;;)pętli:

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

for(;;)Działało dla 800 zagnieżdżonych , ale dawało ten sam błąd, który napotkał Marco13 podczas próbowania 900 pętli.

Kiedy się kompiluje, for(;;)wydaje się, że blokuje wątek bez maksymalnego wykorzystania procesora; powierzchownie wydaje się działać jak plik Thread.Sleep().

Nat
źródło
2
„kończy działanie prawie natychmiast” - to dziwne - myślałem, że for(;;)to będzie nieskończona pętla w C # ?! (Nie mogę tego teraz wypróbować, ale jeśli okaże się, że jest źle, usunę ten komentarz)
Marco13
@ Marco13: Tak, masz rację! Nie jestem pewien, co działo się wcześniej w moim kodzie testowym, ale for(;;)blokuje się bez zużywania czasu procesora.
Nat,
Pętla for (;;) to nieskończona pętla, więc po co dodawać kolejną pętlę for (;;) lub 800 z nich, ponieważ nie zostaną uruchomione. pierwszy będzie trwał wiecznie.
Fred Smith
1
@FredSmith: for(;;)Pętle są zagnieżdżone, więc wszystkie zaczynają się od najbardziej zagnieżdżonej pętli, która jest nieskończona. for(;;)Musiałby to być pierwszy blok for(;;){}. Jeśli chodzi o to, dlaczego to robi, był to tylko test, aby zobaczyć, jakie ograniczenia ma bieżąca implementacja .NET / C #; najwyraźniej nie może dojść do 900 for(;;), chociaż, jak zauważyłeś, nic nie robi.
Nat