Czy należy użyć <lub <= w pętli for [zamknięta]


124

Gdybyś musiał powtórzyć pętlę 7 razy, użyłbyś:

for (int i = 0; i < 7; i++)

lub:

for (int i = 0; i <= 6; i++)

Istnieją dwie kwestie:

  • występ
  • czytelność

Dla wydajności zakładam Java lub C #. Czy ma znaczenie, czy użyto „mniejszy niż” lub „mniejszy lub równy”? Jeśli masz wgląd w inny język, wskaż który.

Dla czytelności zakładam tablice oparte na 0.

UPD: Moja wzmianka o tablicach opartych na 0 może być myląca. Nie mówię o iterowaniu przez elementy tablicy. Tylko ogólna pętla.

Poniżej znajduje się dobry punkt na temat używania stałej, do której można by wyjaśnić, czym jest ta magiczna liczba. Więc gdybym miał " int NUMBER_OF_THINGS = 7to" i <= NUMBER_OF_THINGS - 1wyglądałoby dziwnie, prawda?


powiedziałbym: jeśli przejdziesz przez całą tablicę, nigdy nie odejmuj ani nie dodawaj żadnej liczby po lewej stronie.
Letterman

Odpowiedzi:


287

Pierwsza jest bardziej idiomatyczna . W szczególności wskazuje (w sensie zerowym) liczbę iteracji. Podczas korzystania z czegoś opartego na 1 (np. JDBC, IIRC) mógłbym ulec pokusie użycia <=. Więc:

for (int i=0; i < count; i++) // For 0-based APIs

for (int i=1; i <= count; i++) // For 1-based APIs

Spodziewałbym się, że różnica w wydajności będzie nieznacznie mała w rzeczywistym kodzie.


30
Masz prawie gwarancję, że nie będzie różnicy w wydajności. Wiele architektur, takich jak x86, ma instrukcje „skacz po mniejszym lub równym w ostatnim porównaniu”. Najbardziej prawdopodobnym sposobem zauważenia różnicy w wydajności byłby jakiś rodzaj interpretowanego języka, który został źle zaimplementowany.
Wedge

3
Czy zamiast tego rozważyłbyś użycie! =? Powiedziałbym, że to najwyraźniej ustala i jako licznik pętli i nic więcej.
yungchin

21
Zwykle bym tego nie zrobił. To po prostu zbyt obce. Grozi również przejściem w bardzo, bardzo długą pętlę, jeśli ktoś przypadkowo zwiększy wartość i podczas pętli.
Jon Skeet

5
Programowanie ogólne z iteratorami STL wymaga użycia! =. To (przypadkowe dwukrotne zwiększenie) nie było dla mnie problemem. Zgadzam się, że dla indeksów <(lub> malejąco) są bardziej przejrzyste i konwencjonalne.
Jonathan Graehl

2
Pamiętaj, że jeśli zapętlisz długość tablicy przy użyciu <, JIT optymalizuje dostęp do tablicy (usuwa powiązane kontrole). Powinno więc być szybsze niż użycie <=. Nie sprawdziłem tego
konfigurator

72

Obie te pętle iterują 7 razy. Powiedziałbym, że ten z 7 jest bardziej czytelny / wyraźniejszy, chyba że masz naprawdę dobry powód do tego drugiego.


Pamiętam, kiedy po raz pierwszy zacząłem uczyć się języka Java. Nienawidziłem koncepcji indeksu opartego na 0, ponieważ zawsze używałem indeksów opartych na 1. Dlatego zawsze używałbym wariantu <= 6 (jak pokazano w pytaniu). Na moją szkodę, ponieważ w końcu bardziej zmyliłbym mnie, kiedy pętla for faktycznie się zakończyła. Łatwiej jest po prostu użyć <
James Haug,

55

Pamiętam z moich czasów, kiedy robiliśmy 8086 Assembly na uczelni, było bardziej wydajne do zrobienia:

for (int i = 6; i > -1; i--)

ponieważ była operacja JNS , która oznacza skok, jeśli nie ma znaku. Użycie tego oznaczało, że po każdym cyklu nie było przeszukiwania pamięci, aby uzyskać wartość porównawczą, ani porównania. Obecnie większość kompilatorów optymalizuje użycie rejestrów, więc kwestia pamięci nie jest już ważna, ale nadal uzyskuje się niepotrzebne porównanie.

Nawiasem mówiąc, wstawienie 7 lub 6 do pętli wprowadza „ magiczną liczbę ”. Dla lepszej czytelności powinieneś używać stałej z nazwą ujawniającą intencję. Lubię to:

const int NUMBER_OF_CARS = 7;
for (int i = 0; i < NUMBER_OF_CARS; i++)

EDYCJA: Ludzie nie rozumieją kwestii montażu, więc oczywiście wymagany jest pełniejszy przykład:

Jeśli zrobimy dla (i = 0; i <= 10; i ++), musisz to zrobić:

    mov esi, 0
loopStartLabel:
                ; Do some stuff
    inc esi
                ; Note cmp command on next line
    cmp esi, 10
    jle exitLoopLabel
    jmp loopStartLabel
exitLoopLabel:

Jeśli zrobimy dla (int i = 10; i> -1; i--), to możesz uciec z tego:

    mov esi, 10
loopStartLabel:
                ; Do some stuff
    dec esi
                ; Note no cmp command on next line
    jns exitLoopLabel
    jmp loopStartLabel
exitLoopLabel:

Właśnie sprawdziłem i kompilator C ++ firmy Microsoft nie wykonuje tej optymalizacji, ale robi to, jeśli:

for (int i = 10; i >= 0; i--) 

Więc morał jest taki, że jeśli używasz Microsoft C ++ †, a rosnąco lub malejąco nie robi różnicy, aby uzyskać szybką pętlę, powinieneś użyć:

for (int i = 10; i >= 0; i--)

zamiast któregokolwiek z tych:

for (int i = 10; i > -1; i--)
for (int i = 0; i <= 10; i++)

Ale szczerze mówiąc, uzyskanie czytelności „for (int i = 0; i <= 10; i ++)” jest zwykle znacznie ważniejsze niż brak jednego polecenia procesora.

† Inne kompilatory mogą robić różne rzeczy.


4
Przypadek „magicznej liczby” dobrze ilustruje, dlaczego zwykle lepiej jest użyć <niż <=.
Rene Saarsoo

11
Inna wersja to „for (int i = 10; i--;)”. Niektórzy używają "for (int i = 10; i -> 0;)" i udają, że kombinacja -> oznacza, że ​​idzie do.
Zayenz

2
+1 za kod montażowy
neuro

1
ale kiedy nadejdzie czas, aby faktycznie używać licznika pętli, np. do indeksowania tablic, musisz zrobić, 7-ico spowoduje zablokowanie wszelkich optymalizacji, które uzyskasz od liczenia wstecz.
Lie Ryan

@Lie, dotyczy to tylko sytuacji, gdy musisz przetworzyć elementy w kolejności do przodu. W przypadku większości operacji w tego rodzaju pętlach można je zastosować do elementów w pętli w dowolnej kolejności. Na przykład, jeśli szukasz wartości, nie ma znaczenia, czy zaczniesz na końcu listy i przejdziesz do góry, czy na początku listy i przeanalizujesz (zakładając, że nie możesz przewidzieć, który koniec listy jest być i buforowanie pamięci nie jest problemem).
Martin Brown

27

Zawsze używam <array.length, ponieważ jest łatwiejsza do odczytania niż <= array.length-1.

również mając <7 i biorąc pod uwagę, że wiesz, że zaczyna się od indeksu 0, powinno być intuicyjne, że liczba jest liczbą iteracji.


Zawsze należy uważać, aby sprawdzić koszt funkcji Length podczas używania ich w pętli. Na przykład, jeśli używasz strlen w C / C ++, znacznie wydłużysz czas potrzebny na wykonanie porównania. Dzieje się tak, ponieważ strlen musi iterować cały ciąg, aby znaleźć odpowiedź, co jest czymś, co prawdopodobnie chcesz zrobić tylko raz, a nie dla każdej iteracji pętli.
Martin Brown

2
@Martin Brown: w Javie (i wierzę w C #), String.length i Array.length są stałe, ponieważ String jest niezmienny, a Array ma niezmienną długość. A ponieważ String.length i Array.length to pole (zamiast wywołania funkcji), możesz być pewien, że muszą mieć wartość O (1). W przypadku C ++, cóż, dlaczego do cholery używasz w ogóle C-string?
Lie Ryan

18

Z optymalizacyjnego punktu widzenia nie ma to znaczenia.

Z punktu widzenia stylu kodu, który preferuję <. Powód:

for ( int i = 0; i < array.size(); i++ )

jest o wiele bardziej czytelny niż

for ( int i = 0; i <= array.size() -1; i++ )

także <od razu podaje liczbę iteracji.

Innym głosem za <jest to, że możesz zapobiec wielu przypadkowym pomyłkom.


10

@Chris, Twoje stwierdzenie, że .Długość jest kosztowna w .NET jest w rzeczywistości nieprawdą, aw przypadku prostych typów jest dokładnie odwrotnie.

int len = somearray.Length;
for(i = 0; i < len; i++)
{
  somearray[i].something();
}

jest faktycznie wolniejszy niż

for(i = 0; i < somearray.Length; i++)
{
  somearray[i].something();
}

Ta ostatnia to przypadek zoptymalizowany przez środowisko wykonawcze. Ponieważ środowisko wykonawcze może zagwarantować, że i jest prawidłowym indeksem tablicy, nie są wykonywane żadne kontrole ograniczeń. W pierwszym przypadku środowisko uruchomieniowe nie może zagwarantować, że nie zostałem zmodyfikowany przed zapętleniem i wymusza sprawdzanie granic tablicy przy każdym wyszukiwaniu indeksu.


W języku Java .Length może w niektórych przypadkach być kosztowna. stackoverflow.com/questions/6093537/for-loop-optimization b'coz wywołuje .lenghtOR .size. Nie jestem pewien, ale i nie jestem pewien, ale chcę się tylko upewnić.
Ravi Parekh

6

Nie ma to żadnego znaczenia, jeśli chodzi o wydajność. Dlatego użyłbym tego, co jest łatwiejsze do zrozumienia w kontekście problemu, który rozwiązujesz.


5

Wolę:

for (int i = 0; i < 7; i++)

Myślę, że łatwiej to tłumaczy się na „powtarzanie pętli 7 razy”.

Nie jestem pewien wpływu na wydajność - podejrzewam, że wszelkie różnice zostałyby zestawione.


4

W Javie 1.5 możesz to zrobić

for (int i: myArray) {
    ...
}

więc w przypadku tablicy nie musisz się martwić.


4

Myślę, że nie ma różnicy w wydajności. Druga forma jest jednak zdecydowanie bardziej czytelna, nie musisz mentalnie odejmować jednej, aby znaleźć ostatnią liczbę iteracji.

EDYCJA: Widzę, że inni się nie zgadzają. Dla mnie osobiście lubię widzieć rzeczywiste numery indeksów w strukturze pętli. Może dlatego, że bardziej przypomina 0..6składnię Perla , która, jak wiem, jest równoważna (0,1,2,3,4,5,6). Jeśli widzę 7, muszę sprawdzić operator obok, aby zobaczyć, że w rzeczywistości indeks 7 nigdy nie został osiągnięty.


Zależy to od tego, czy uważasz, że „numer ostatniej iteracji” jest ważniejszy niż „liczba iteracji”. Z API opartym na 0, zawsze będą się różnić o 1 ...
Jon Skeet

4

Powiedziałbym, że używaj wersji "<7", ponieważ większość ludzi to przeczyta - więc jeśli ludzie będą przeglądać twój kod, mogą go źle zinterpretować.

Nie martwiłbym się, czy „<” jest szybsze niż „<=”, po prostu wybierz czytelność.

Jeśli chcesz zwiększyć prędkość, rozważ następujące kwestie:

for (int i = 0; i < this->GetCount(); i++)
{
  // Do something
}

Aby zwiększyć wydajność, możesz nieznacznie zmienić jego kolejność na:

const int count = this->GetCount();
for (int i = 0; i < count; ++i)
{
  // Do something
}

Zwróć uwagę na usunięcie GetCount () z pętli (ponieważ będzie to sprawdzane w każdej pętli) i zmianę „i ++” na „++ i”.


Drugi podoba mi się bardziej, ponieważ jest łatwiejszy do odczytania, ale czy naprawdę przelicza to-> GetCount () za każdym razem? Zostałem złapany przez to, gdy zmieniam this i licznik pozostaje taki sam, zmuszając mnie do zrobienia ... podczas tego-> GetCount ()
osp70

GetCount () byłaby wywoływana w każdej iteracji w pierwszym przykładzie. W drugim przykładzie zostałaby wywołana tylko raz. Jeśli chcesz, aby GetCount () był wywoływany za każdym razem, miej go w pętli, jeśli nie próbujesz trzymać go na zewnątrz.
Mark Ingram

Tak, wypróbowałem to i masz rację, przepraszam.
osp70

Jaką różnicę ma używanie ++ i zamiast i ++?
Rene Saarsoo

Niewielki wzrost szybkości podczas używania ints, ale wzrost może być większy, jeśli zwiększasz własne klasy. Zasadniczo ++ i zwiększa wartość rzeczywistą, a następnie zwraca wartość rzeczywistą. i ++ tworzy zmienną temp, inkrementuje wartość real var, a następnie zwraca temp. Żadne tworzenie var nie jest konieczne z ++ i.
Mark Ingram

4

W C ++ wolę używać !=, co jest możliwe do użytku ze wszystkimi kontenerami STL. Nie wszystkie iteratory kontenerów STL są mniej niż porównywalne.


To mnie trochę przeraża tylko dlatego, że istnieje bardzo niewielka zewnętrzna szansa, że ​​coś może powtórzyć licznik po mojej zamierzonej wartości, co z kolei powoduje, że jest to nieskończona pętla. Szanse są niewielkie i łatwe do wykrycia - ale < czuje się bezpieczniej.
Rob Allen

2
Jeśli w twoim kodzie jest taki błąd, prawdopodobnie lepiej się zawiesić i spalić niż po cichu kontynuować :-)

To jest właściwa odpowiedź: stawia mniejsze zapotrzebowanie na Twój iterator i jest bardziej prawdopodobne, że pojawi się, jeśli w kodzie wystąpi błąd. Argument za <jest krótkowzroczny. Może nieskończona pętla byłaby zła w latach 70-tych, kiedy płaciłeś za czas procesora. '! =' rzadziej ukrywa błąd.
David Nehme

1
Pętla po iteratorach to zupełnie inny przypadek niż zapętlanie z licznikiem. ! = jest niezbędna dla iteratorów.
DJClayworth

4

Edsger Dijkstra napisał artykuł na ten temat w 1982 roku, w którym argumentował za niższym <= i <górnym:

Istnieje najmniejsza liczba naturalna. Wykluczenie dolnej granicy - jak w b) id) - wymusza podciąg zaczynający się od najmniejszej liczby naturalnej, niższa granica, jak wspomniano, do sfery liczb nienaturalnych. To jest brzydkie, więc dla dolnej granicy wolimy ≤ jak w a) ic). Rozważmy teraz podciągi zaczynające się od najmniejszej liczby naturalnej: włączenie górnej granicy wymusiłoby wówczas, że ta ostatnia byłaby nienaturalna do czasu, gdy sekwencja skurczyłaby się do pustej. To jest brzydkie, więc dla górnej granicy wolimy <jak w a) id). Wnioskujemy, że preferowana jest konwencja a).


3

Po pierwsze, nie używaj 6 ani 7.

Lepiej używać:

int numberOfDays = 7;
for (int day = 0; day < numberOfDays ; day++){

}

W tym przypadku jest to lepsze niż używanie

for (int day = 0; day <= numberOfDays  - 1; day++){

}

Jeszcze lepiej (Java / C #):

for(int day = 0; day < dayArray.Length; i++){

}

A nawet lepiej (C #)

foreach (int day in days){// day : days in Java

}

Pętla odwrotna jest rzeczywiście szybsza, ale ponieważ jest trudniejsza do odczytania (jeśli nie przez Ciebie innych programistów), lepiej jej unikać. Szczególnie w C #, Javie ...


2

Zgadzam się z tłumem, który mówi, że 7 ma sens w tym przypadku, ale dodam, że w przypadku, gdy 6 jest ważna, powiedz, że chcesz wyjaśnić, że działasz tylko na obiektach do szóstego indeksu, a następnie <= jest lepsze, ponieważ sprawia, że ​​6 jest łatwiejsze do zobaczenia.


tak, i <rozmiar w porównaniu do i <= LAST_FILLED_ARRAY_SLOT
Chris Cudmore

2

Jeszcze na studiach pamiętam coś o tych dwóch operacjach, które były podobne pod względem czasu obliczeniowego procesora. Oczywiście rozmawiamy na poziomie montażu.

Jeśli jednak mówisz w języku C # lub Javie, naprawdę nie sądzę, aby jeden z nich był przyspieszeniem w stosunku do drugiego. Kilka nanosekund, które uzyskasz, najprawdopodobniej nie jest warte żadnego zamieszania, które wprowadzasz.

Osobiście napisałbym kod, który ma sens z punktu widzenia implementacji biznesowej i upewnić się, że jest łatwy do odczytania.


2

To bezpośrednio należy do kategorii „Poprawianie niewłaściwego wyglądu kodu” .

W językach indeksowania od zera, takich jak Java lub C #, ludzie są przyzwyczajeni do odmian index < count warunku. Tak więc wykorzystanie tej konwencji defacto sprawiłoby, że jeden błąd byłby bardziej oczywisty.

Jeśli chodzi o wydajność: każdy dobry kompilator wart swojego zużycia pamięci powinien renderować bez problemu.


2

Na marginesie, gdy przeglądam tablicę lub inną kolekcję w .Net, znajduję

foreach (string item in myarray)
{
    System.Console.WriteLine(item);
}

być bardziej czytelnym niż numeryczna pętla for. To oczywiście zakłada, że ​​rzeczywisty licznik Int sam nie jest używany w kodzie pętli. Nie wiem, czy nastąpiła zmiana wydajności.


Wymaga to również, aby nie modyfikować rozmiaru kolekcji podczas pętli.
Jeff B

Tak też byłoby For (i = 0, i <myarray.count, i ++)
Rob Allen

1

Istnieje wiele dobrych powodów, aby pisać i <7. Posiadanie liczby 7 w pętli, która powtarza się 7 razy, jest dobre. Wydajność jest faktycznie identyczna. Prawie każdy pisze i <7. Jeśli piszesz dla większej czytelności, użyj formularza, który wszyscy natychmiast rozpoznają.


1

Zawsze wolałem:

for ( int count = 7 ; count > 0 ; -- count )

Jakie masz uzasadnienie? Jestem szczerze zainteresowany.
Chris Cudmore

to idealnie nadaje się do odwrotnej pętli ... jeśli kiedykolwiek będziesz potrzebować czegoś takiego
ShoeLace

1
Jednym z powodów jest to, że na poziomie uP porównanie do 0 jest szybkie. Innym jest to, że dla mnie dobrze się czyta, a liczba daje mi łatwe wskazanie, ile razy pozostało.
kenny

1

Nawyk używania <sprawi, że będzie on spójny zarówno dla Ciebie, jak i dla czytelnika podczas iteracji po tablicy. Każdemu będzie łatwiej mieć standardową konwencję. A jeśli używasz języka z tablicami opartymi na 0, to <jest konwencją.

To prawie na pewno ma większe znaczenie niż jakakolwiek różnica w wydajności między <i <=. Najpierw postaw na funkcjonalność i czytelność, a następnie zoptymalizuj.

Inną uwagą jest to, że lepiej byłoby mieć zwyczaj robienia ++ i zamiast i ++, ponieważ pobieranie i inkrementacja wymaga tymczasowości, a inkrementacja nie. W przypadku liczb całkowitych Twój kompilator prawdopodobnie zoptymalizuje tymczasowo, ale jeśli typ iteracji jest bardziej złożony, może nie być w stanie tego zrobić.


1

Nie używaj magicznych liczb.

Dlaczego jest 7? (lub 6 jeśli o to chodzi).

użyj właściwego symbolu dla numeru, którego chcesz użyć ...

W takim przypadku myślę, że lepiej jest użyć

for ( int i = 0; i < array.size(); i++ )

1

Operatory „<” i „<=” mają dokładnie taki sam koszt wydajności.

Operator „<” jest standardowym i łatwiejszym do odczytania w pętli liczonej od zera.

Używanie ++ i zamiast i ++ poprawia wydajność w C ++, ale nie w C # - nie znam Javy.


1

Jak ludzie zauważyli, nie ma różnicy w żadnej z dwóch alternatyw, o których wspomniałeś. Aby to potwierdzić, wykonałem proste testy porównawcze w JavaScript.

Wyniki możesz zobaczyć tutaj . Nie jest z tego jasne, że jeśli zamieniam pozycję pierwszego i drugiego testu, to wyniki tych dwóch testów zamieniają się, jest to ewidentnie problem z pamięcią. Jednak trzeci test, w którym odwracam kolejność iteracji, jest wyraźnie szybszy.


dlaczego zaczynasz od i = 1 w drugim przypadku?
Eugene Katz,

Hrmm, prawdopodobnie głupi błąd? Podniosłem to dość szybko, może 15 minut.
David Wees

1

Jak wszyscy mówią, zwyczajowo używa się iteratorów indeksowanych przez 0, nawet dla rzeczy poza tablicami. Jeśli wszystko zaczyna się na 0i kończy się na n-1, a dolne granice są zawsze, <=a górne granice zawsze <, to jest o wiele mniej myślenia, które musisz robić podczas przeglądania kodu.


1

Świetne pytanie. Moja odpowiedź: użyj typu A („<”)

  • Wyraźnie widzisz, ile masz iteracji (7).
  • Różnica między dwoma punktami końcowymi to szerokość zakresu
  • Mniej znaków sprawia, że ​​jest bardziej czytelny
  • Częściej masz całkowitą liczbę elementów, i < strlen(s)a nie indeks ostatniego elementu, więc jednolitość jest ważna.

Inny problem dotyczy całej tej konstrukcji. ipojawia się w nim 3 razy , więc można go pomylić. Konstrukcja pętli for mówi, jak zrobić, a nie co robić . Proponuję przyjąć to:

BOOST_FOREACH(i, IntegerInterval(0,7))

Jest to bardziej przejrzyste, kompiluje się dokładnie takie same instrukcje asm, itp. Zapytaj mnie o kod IntegerInterval, jeśli chcesz.


1

Tyle odpowiedzi ... ale wydaje mi się, że mam coś do dodania.

Wolę, aby liczby dosłowne były wyraźnie widoczne jakie wartości „i” przyjmie w pętli . Tak więc w przypadku iteracji przez tablicę od zera:

for (int i = 0; i <= array.Length - 1; ++i)

A jeśli po prostu zapętlasz, a nie iterujesz po tablicy, liczenie od 1 do 7 jest całkiem intuicyjne:

for (int i = 1; i <= 7; ++i)

Czytelność ma przewagę nad wydajnością, dopóki jej nie sprofilujesz, ponieważ prawdopodobnie nie wiesz, co kompilator lub środowisko wykonawcze zrobi z Twoim kodem do tego czasu.


1

Możesz także użyć !=zamiast tego. W ten sposób otrzymasz nieskończoną pętlę, jeśli popełnisz błąd podczas inicjalizacji, co spowoduje wcześniejsze zauważenie błędu, a wszelkie problemy, które spowoduje, będą ograniczone do utknięcia w pętli (zamiast mieć problem znacznie później i nie znajdując to).


0

Myślę, że jedno lub drugie jest w porządku, ale kiedy już wybierzesz, trzymaj się jednego lub drugiego. Jeśli jesteś przyzwyczajony do używania <=, spróbuj nie używać <i na odwrót.

Wolę <=, ale w sytuacjach, w których pracujesz z indeksami zaczynającymi się od zera, prawdopodobnie spróbuję użyć <. Jednak to wszystko zależy od osobistych preferencji.


0

Ściśle z logicznego punktu widzenia, musisz pomyśleć, że < countbyłoby to bardziej wydajne niż <= countz tego dokładnego powodu, który <=będzie testował również pod kątem równości.


Nie sądzę, w asemblerze sprowadza się to do cmp eax, 7 jl LOOP_START lub cmp eax, 6 jle LOOP_START oba wymagają tej samej liczby cykli.
Treb

<= można zaimplementować jako! (>)
JohnMcG

! (>) to nadal dwie instrukcje, ale Treb ma rację, że JLE i JL używają tej samej liczby cykli zegara, więc <i <= zajmują taką samą ilość czasu.
Jacob Krall
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.