Ten pomysł pochodzi od tego pytania na stackoverflow.com
Powszechny jest następujący wzór:
final x = 10;//whatever constant value
for(int i = 0; i < Math.floor(Math.sqrt(x)) + 1; i++) {
//...do something
}
Chcę, aby stwierdzenie warunkowe było czymś skomplikowanym i nie zmienia się.
Czy lepiej jest zadeklarować go w sekcji inicjalizacji pętli jako takiej?
final x = 10;//whatever constant value
for(int i = 0, j = Math.floor(Math.sqrt(x)) + 1; i < j; i++) {
//...do something
}
Czy to jest bardziej jasne?
Co jeśli wyrażenie warunkowe jest proste, takie jak
final x = 10;//whatever constant value
for(int i = 0, j = n*n; i > j; j++) {
//...do something
}
java
c++
coding-style
language-agnostic
Celeritas
źródło
źródło
x
jest duży,Math.floor(Math.sqrt(x))+1
jest równyMath.floor(Math.sqrt(x))
. :-){ x=whatever; for (...) {...} }
Lub, jeszcze lepiej, zastanów się, czy wystarczająco dużo dzieje się w tym, że musi to być osobna funkcja.Odpowiedzi:
Zrobiłbym coś takiego:
Szczerze mówiąc, jedynym dobrym powodem, aby wcisnąć
j
(terazlimit
) wciskanie w nagłówek pętli, jest utrzymanie odpowiedniego zakresu. Wszystko, czego potrzeba, aby sprawić, że nie będzie problemu, to ładny, ciasno zamknięty zakres.Potrafię docenić chęć bycia szybkim, ale nie poświęcam czytelności bez prawdziwego powodu.
Jasne, kompilator może zoptymalizować, inicjowanie wielu zmiennych może być legalne, ale pętle są wystarczająco trudne do debugowania. Proszę bądź miły dla ludzi. Jeśli to naprawdę nas spowalnia, dobrze jest to zrozumieć na tyle, aby to naprawić.
źródło
for(int i = 0; i < n*n; i++){...}
nie przypisałbyśn*n
do zmiennej, prawda?final
). Kogo to obchodzi, jeśli stała, która ma wymuszanie kompilatora uniemożliwiające jej zmianę, jest dostępna później w funkcji?Dobry kompilator wygeneruje ten sam kod w obu kierunkach, więc jeśli dążysz do wydajności, dokonaj zmiany tylko wtedy, gdy znajduje się ona w krytycznej pętli i rzeczywiście ją profilowałeś i okazało się, że to robi różnicę. Nawet jeśli kompilator nie może go zoptymalizować, jak ludzie zauważyli w komentarzach dotyczących sprawy z wywołaniami funkcji, w zdecydowanej większości przypadków różnica w wydajności będzie zbyt mała, aby była warta rozważenia przez programistę.
Jednak...
Nie możemy zapominać, że kod jest przede wszystkim środkiem komunikacji między ludźmi i obie opcje nie komunikują się zbyt dobrze z innymi ludźmi. Pierwszy sprawia wrażenie, że wyrażenie musi być obliczane przy każdej iteracji, a drugi znajdujący się w sekcji inicjalizacji oznacza, że zostanie zaktualizowany gdzieś w pętli, gdzie jest naprawdę stały.
W rzeczywistości wolałbym, aby został wyciągnięty ponad pętlę i uczyniony,
final
aby było to natychmiast i obficie jasne dla każdego, kto czyta kod. Nie jest to również idealne, ponieważ zwiększa zakres zmiennej, ale twoja funkcja zamykająca nie powinna zawierać znacznie więcej niż tej pętli.źródło
Jak powiedział @Karl Bielefeldt w swojej odpowiedzi, zwykle nie stanowi to problemu.
Jednak kiedyś był to powszechny problem w C i C ++ i pojawiła się sztuczka, która pomogła rozwiązać problem bez zmniejszania czytelności kodu - iteracja wstecz, aż do
0
.Teraz warunkiem każdej iteracji jest dokładnie to,
>= 0
co każdy kompilator skompiluje w 1 lub 2 instrukcjach montażu. Każdy procesor wykonany w ciągu ostatnich kilku dekad powinien mieć podstawowe kontrole takie jak te; Robiąc szybkie sprawdzenie na mojej maszynie x64, widzę, że to przewidywalnie zmienia się wcmpl $0x0, -0x14(%rbp)
(Long-int-porównaj wartość 0 vs. rejestr rbp offsetowany -14) ijl 0x100000f59
(przeskocz do instrukcji po pętli, jeśli poprzednie porównanie było prawdziwe dla „2nd-arg <1st-arg ”) .Zauważ, że usunąłem
+ 1
zMath.floor(Math.sqrt(x)) + 1
; aby matematyka się powiodła, wartość początkowa powinna wynosićint i = «iterationCount» - 1
. Warto również zauważyć, że twój iterator musi być podpisany;unsigned int
nie będzie działać i prawdopodobnie ostrzega kompilatora.Po programowaniu w językach opartych na języku C przez ~ 20 lat piszę teraz tylko pętle iteracyjne odwrotnego indeksowania, chyba że istnieje konkretny powód, aby iterować indeksujący dalej. Oprócz prostszych kontroli warunków, iteracja odwrotna często prowadzi również kroki boczne, co w innym przypadku byłoby kłopotliwymi mutacjami macierzowymi podczas iteracji.
źródło
unsigned
liczniki będą działać tutaj, jeśli zmodyfikujesz czek (najłatwiejszym sposobem jest dodanie tej samej wartości po obu stronach); Na przykład, dla każdego ubytkuDec
, kontrola(i + Dec) >= Dec
powinna zawsze mieć taki sam wynik jaksigned
odprawyi >= 0
, zarównosigned
iunsigned
liczników, o ile język ma dobrze zdefiniowane zasady wraparound dlaunsigned
zmiennych (konkretnie,-n + n == 0
musi być prawdą zarówno dlasigned
iunsigned
). Należy jednak pamiętać, że może to być mniej wydajne niż>=0
czek podpisany , jeśli kompilator nie ma dla niego optymalizacji.Dec
stałej zarówno do wartości początkowej, jak i końcowej działa, ale czyni ją mniej intuicyjną, a jeśli używaszi
jako indeksu tablicowego, musisz również zrobićunsigned int arrayI = i - Dec;
w ciele pętli. Po prostu używam iteracji do przodu, gdy utknąłem z niepodpisanym iteratorem; często zi <= count - 1
warunkiem, aby logika była równoległa do pętli odwrotnej iteracji.Dec
konkretnie wartości początkowej i końcowej, ale przesunięcie sprawdzania stanuDec
po obu stronach.for (unsigned i = N - 1; i + 1 >= 1; i--) /*...*/
Pozwala toi
normalnie korzystać z pętli, jednocześnie gwarantując, że najniższa możliwa wartość po lewej stronie warunku to0
(aby zapobiec zakłócaniu zawijania). To na pewno dużo prostsze w użyciu iteracji do przodu podczas pracy z niepodpisanych liczników, choć.Staje się interesujący, gdy Math.sqrt (x) zostanie zastąpiony przez Mymodule.SomeNonPureMethodWithSideEffects (x).
Zasadniczo moim sposobem działania jest: jeśli oczekuje się, że zawsze da tę samą wartość, to oceń to tylko raz. Na przykład List.Count, jeśli lista ma się nie zmieniać podczas działania pętli, przenieś licznik poza pętlę do innej zmiennej.
Niektóre z tych „liczeń” mogą być zaskakująco drogie, szczególnie w przypadku baz danych. Nawet jeśli pracujesz nad zestawem danych, który nie powinien ulec zmianie podczas iteracji listy.
źródło
for( auto it = begin(dataset); !at_end(it); ++it )
Moim zdaniem jest to bardzo specyficzne dla języka. Na przykład, jeśli użyję C ++ 11, podejrzewam, że gdyby funkcja sprawdzania warunków była
constexpr
funkcją, kompilator bardzo prawdopodobnie zoptymalizowałby wiele wykonań, ponieważ wie, że za każdym razem uzyska tę samą wartość.Jeśli jednak wywołanie funkcji jest funkcją biblioteki, która nie jest
constexpr
kompilatorem, prawie na pewno wykona ją przy każdej iteracji, ponieważ nie może tego wywnioskować (chyba że jest wbudowane i dlatego można ją wydedukować jako czyste).Wiem mniej o Javie, ale biorąc pod uwagę, że jest to skompilowany JIT, zgaduję, że kompilator ma wystarczająco dużo informacji w czasie wykonywania, aby prawdopodobnie wbudować i zoptymalizować ten warunek. Ale opierałoby się to na dobrej konstrukcji kompilatora, a kompilator decydujący o tej pętli był priorytetem optymalizacji, którego możemy się tylko domyślać.
Osobiście uważam, że jest nieco bardziej elegancki postawić warunek wewnątrz pętli for, jeśli możesz, ale jeśli jest to kompleks będę pisać go w
constexpr
lubinline
funkcji, lub twoi języki equivalant do zrozumienia, że funkcja ta jest czysta i optimisable. To sprawia, że intencja jest oczywista i utrzymuje styl idiomatycznej pętli bez tworzenia ogromnej, nieczytelnej linii. Daje również warunek sprawdzania nazwy, jeśli jest to jego własna funkcja, dzięki czemu czytelnicy mogą natychmiast zobaczyć logicznie, do czego służy sprawdzanie, bez odczytywania go, jeśli jest złożony.źródło