Sollten nicht-triviale bedingte Anweisungen in den Initialisierungsabschnitt von Schleifen verschoben werden?

21

Ich habe diese Idee aus dieser Frage auf stackoverflow.com

Das folgende Muster ist üblich:

final x = 10;//whatever constant value
for(int i = 0; i < Math.floor(Math.sqrt(x)) + 1; i++) {
  //...do something
}

Der Punkt, den ich versuche, ist, dass die bedingte Aussage etwas Kompliziertes ist und sich nicht ändert.

Ist es besser, es im Initialisierungsabschnitt der Schleife als solches zu deklarieren?

final x = 10;//whatever constant value
for(int i = 0, j = Math.floor(Math.sqrt(x)) + 1; i < j; i++) {
  //...do something
}

Ist das klarer?

Was ist, wenn der bedingte Ausdruck einfach ist wie

final x = 10;//whatever constant value
for(int i = 0, j = n*n; i > j; j++) {
  //...do something
}
Celeritas
quelle
47
Warum nicht einfach in die Zeile vor der Schleife verschieben, dann können Sie ihr auch einen sinnvollen Namen geben.
Jonrsharpe
2
@Mehrdad: Sie sind nicht gleichwertig. Wenn xin der Größe groß ist, Math.floor(Math.sqrt(x))+1ist gleich Math.floor(Math.sqrt(x)). :-)
R ..
5
@jonrsharpe Weil das den Gültigkeitsbereich der Variablen erweitern würde. Ich sage nicht unbedingt, dass das ein guter Grund ist, es nicht zu tun, aber das ist der Grund, warum manche Leute es nicht tun.
Kevin Krumwiede
3
@KevinKrumwiede Wenn der Geltungsbereich ein Problem darstellt, beschränken Sie ihn, indem Sie den Code in einen eigenen Block einfügen, z { x=whatever; for (...) {...} }.
Blrfl
2
@jonrsharpe Sie können ihm einen sinnvollen Namen geben, wenn Sie ihn auch in der init-Sektion deklarieren. Ohne zu sagen, dass ich es dort hinstellen würde; es ist immer noch einfacher zu lesen, wenn es getrennt ist.
JollyJoker

Antworten:

62

Was ich tun würde, ist ungefähr so:

void doSomeThings() {
    final x = 10;//whatever constant value
    final limit = Math.floor(Math.sqrt(x)) + 1;
    for(int i = 0; i < limit; i++) {
         //...do something
    }
}

Ehrlich gesagt, der einzige gute Grund, die Initialisierung j(jetzt limit) in den Loop-Header zu stopfen, besteht darin, den korrekten Gültigkeitsbereich beizubehalten. Alles was es braucht, um eine Nicht-Ausgabe zu machen, ist ein schöner, enger Umfang.

Ich kann den Wunsch, schnell zu sein, würdigen, aber die Lesbarkeit nicht ohne guten Grund opfern.

Sicher, der Compiler kann optimieren, das Initialisieren mehrerer VARs ist zwar zulässig, aber Schleifen sind so schwer zu debuggen. Bitte sei freundlich zu den Menschen. Wenn sich herausstellt, dass dies uns wirklich verlangsamt, ist es schön, es zu verstehen und zu beheben.

kandierte_orange
quelle
Guter Punkt. Ich würde davon ausgehen, dass ich mich nicht mit einer anderen Variablen beschäftige, wenn der Ausdruck etwas Einfaches ist, zum Beispiel würden Sie einer Variablen for(int i = 0; i < n*n; i++){...}keine zuweisen n*n, oder?
Celeritas
1
Ich würde und ich habe. Aber nicht für die Geschwindigkeit. Zur besseren Lesbarkeit. Lesbarer Code ist von sich aus in der Regel schnell.
candied_orange
1
Sogar das Problem des Gültigkeitsbereichs entfällt, wenn Sie als Konstante markieren ( final). Wen interessiert es, ob später in der Funktion auf eine Konstante zugegriffen werden kann, deren Compiler-Erzwingung Änderungen verhindert?
jpmc26
Ich denke, eine große Sache ist das, was Sie erwarten. Ich weiß, dass das Beispiel sqrt verwendet, aber was wäre, wenn es eine andere Funktion wäre? Ist die Funktion rein? Erwarten Sie immer die gleichen Werte? Gibt es Nebenwirkungen? Sind die Nebeneffekte etwas, das Sie bei jeder Iteration passieren möchten?
Pieter B
38

Ein guter Compiler generiert in beiden Fällen den gleichen Code. Wenn Sie also Leistung anstreben, nehmen Sie eine Änderung nur dann vor, wenn sich diese in einer kritischen Schleife befindet und Sie tatsächlich ein Profil erstellt und festgestellt haben, dass sie einen Unterschied macht. Auch wenn der Compiler es nicht optimieren kann, wie in den Kommentaren zum Fall mit Funktionsaufrufen bereits erwähnt, ist der Leistungsunterschied in den allermeisten Situationen zu gering, um eine Überlegung durch einen Programmierer wert zu sein.

Jedoch...

Wir dürfen nicht vergessen, dass Code in erster Linie ein Kommunikationsmedium zwischen Menschen ist und beide Optionen nicht sehr gut mit anderen Menschen kommunizieren. Der erste erweckt den Eindruck, dass der Ausdruck bei jeder Iteration berechnet werden muss, und der zweite im Initialisierungsabschnitt impliziert, dass er irgendwo innerhalb der Schleife aktualisiert wird, wo er wirklich durchgehend konstant ist.

Eigentlich würde ich es vorziehen, wenn es über der Schleife herausgezogen und gemacht wird final, um das jedem, der den Code liest, sofort und in Hülle und Fülle klar zu machen. Das ist auch nicht ideal, weil es den Gültigkeitsbereich der Variablen vergrößert, aber Ihre einschließende Funktion sollte sowieso nicht viel mehr als diese Schleife enthalten.

Karl Bielefeldt
quelle
5
Sobald Sie damit beginnen, Funktionsaufrufe einzuschließen, wird es für den Compiler schwieriger, die Optimierung durchzuführen. Der Compiler müsste speziell wissen, dass Math.sqrt keine Nebenwirkungen hat.
Peter Green
@PeterGreen Wenn dies Java ist, kann die JVM es herausfinden, aber es kann eine Weile dauern.
chrylis -on strike-
Die Frage ist sowohl mit C ++ als auch mit Java markiert. Ich weiß nicht, wie fortgeschritten die Java-JVM ist und wann sie es kann und wann nicht, aber ich weiß, dass C ++ - Compiler es im Allgemeinen nicht herausfinden können also, oder der Körper der Funktion ist sichtbar und alle indirekt aufgerufenen Funktionen können nach den gleichen Kriterien als rein erkannt werden. Hinweis: Nebeneffektfrei reicht nicht aus, um den Loop-Zustand zu verlassen. Funktionen, die vom globalen Status abhängen, können auch dann nicht aus der Schleife verschoben werden, wenn der Schleifenkörper den Status ändern kann.
HDV
Es wird interessant, wenn Math.sqrt (x) durch Mymodule.SomeNonPureMethodWithSideEffects (x) ersetzt wird.
Pieter B
9

Wie @ Karl Bielefeldt in seiner Antwort sagte, ist dies normalerweise kein Problem.

Es war jedoch einmal ein häufiges Problem in C und C ++, und es entstand ein Trick, mit dem das Problem umgangen werden konnte,0 ohne die Lesbarkeit des Codes zu beeinträchtigen .

final x = 10;//whatever constant value
for(int i = Math.floor(Math.sqrt(x)); i >= 0; i--) {
  //...do something
}

Jetzt ist die Bedingung in jeder Iteration, >= 0die jeder Compiler in 1 oder 2 Assembler-Anweisungen kompiliert. Jede CPU, die in den letzten Jahrzehnten hergestellt wurde, sollte grundlegende Überprüfungen wie diese haben. Bei einer kurzen Überprüfung auf meinem x64-Computer wird dies vorhersehbar zu cmpl $0x0, -0x14(%rbp)(Long-Int-Compare-Wert 0 vs. Register-RBP versetzt -14) und jl 0x100000f59(springen Sie zu der Anweisung, die der Schleife folgt, wenn der vorherige Vergleich für "2nd-arg" zutrifft <1st-arg ”) .

Beachten Sie, dass ich das + 1von entfernt habe Math.floor(Math.sqrt(x)) + 1; Damit die Mathematik funktioniert, sollte der Startwert sein int i = «iterationCount» - 1. Bemerkenswert ist auch, dass Ihr Iterator signiert sein muss. unsigned intfunktioniert nicht und wird wahrscheinlich vom Compiler gewarnt.

Nachdem ich ~ 20 Jahre lang in C-basierten Sprachen programmiert habe, schreibe ich jetzt nur noch Reverse-Index-Iterationsschleifen, es sei denn, es gibt einen bestimmten Grund, Index-Iterationen weiterzuleiten. Zusätzlich zu einfacheren Überprüfungen der Bedingungen führt die umgekehrte Iteration häufig auch zu Nebenschritten, was andernfalls störende Array-Mutationen beim Iterieren bedeuten würde.

Slipp D. Thompson
quelle
1
Alles, was Sie schreiben, ist technisch korrekt. Der Kommentar zu Befehlen kann jedoch irreführend sein, da alle modernen CPUs so konzipiert wurden, dass sie auch mit vorwärts iterierenden Schleifen gut funktionieren, unabhängig davon, ob sie einen speziellen Befehl für sie haben. In jedem Fall wird die meiste Zeit innerhalb der Schleife verbracht und keine Iteration durchgeführt.
Jørgen Fogh
4
Es sollte beachtet werden, dass moderne Compileroptimierungen unter Berücksichtigung von "normalem" Code entwickelt wurden. In den meisten Fällen generiert der Compiler Code, der genauso schnell ist, unabhängig davon, ob Sie Optimierungs- "Tricks" verwenden oder nicht. Einige Tricks können jedoch die Optimierung behindern, je nachdem, wie kompliziert sie sind. Wenn die Verwendung bestimmter Tricks die Lesbarkeit Ihres Codes verbessert oder das Erkennen häufiger Fehler erleichtert, ist dies eine großartige Sache. Sie sollten sich jedoch nicht den Kopf zerbrechen, "wenn ich Code auf diese Weise schreibe, wird er schneller". Schreiben Sie Code wie gewohnt und profilieren Sie sich dann, um Orte zu finden, an denen Sie optimieren müssen.
0x5453
Beachten Sie auch, dass unsignedZähler hier funktionieren, wenn Sie die Prüfung ändern (der einfachste Weg ist, auf beiden Seiten den gleichen Wert hinzuzufügen). Beispielsweise sollte Decdie Prüfung für jede Dekrementierung (i + Dec) >= Decimmer dasselbe Ergebnis wie die signedPrüfung i >= 0mit beiden signedund unsignedZählern haben, solange die Sprache gut definierte Umlaufregeln für unsignedVariablen hat (insbesondere -n + n == 0muss für beide signedund wahr sein unsigned). Beachten Sie jedoch, dass dies möglicherweise weniger effizient ist als eine signierte >=0Prüfung, wenn der Compiler keine Optimierung dafür hat.
Justin Time 2 Setzen Sie Monica
1
@ JustinTime Ja, die unterzeichnete Anforderung ist der schwierigste Teil. Das Hinzufügen einer DecKonstante sowohl zum Anfangs- als auch zum Endwert funktioniert, macht ihn jedoch weniger intuitiv. Wenn iSie ihn als Array-Index verwenden, müssen Sie auch einen unsigned int arrayI = i - Dec;In-the-Loop-Body ausführen. Ich benutze nur die Vorwärtsiteration, wenn ich mit einem nicht signierten Iterator stecke. oft mit einer i <= count - 1Bedingung, um die Logik parallel zu den Rückwärtsiterationsschleifen zu halten.
Slipp D. Thompson
1
@SlippD.Thompson Ich wollte nicht speziell Decdie Start- und Endwerte ergänzen , sondern die Zustandsüberprüfung Decauf beiden Seiten verschieben. for (unsigned i = N - 1; i + 1 >= 1; i--) /*...*/ Auf diese Weise können Sie idie Schleife normal verwenden und gleichzeitig sicherstellen, dass auf der linken Seite der Bedingung der niedrigstmögliche Wert 0angezeigt wird (um zu verhindern, dass der Umlauf beeinträchtigt wird). Es ist jedoch definitiv viel einfacher, die Vorwärtsiteration zu verwenden, wenn Sie mit vorzeichenlosen Zählern arbeiten.
Justin Time 2 Setzen Sie Monica
3

Interessant wird es, wenn Math.sqrt (x) durch Mymodule.SomeNonPureMethodWithSideEffects (x) ersetzt wird.

Grundsätzlich ist mein Modus operandi: Wenn erwartet wird, dass etwas immer den gleichen Wert ergibt, dann bewerte es nur einmal. Zum Beispiel List.Count, wenn sich die Liste während des Betriebs der Schleife nicht ändern soll, wird die Zählung außerhalb der Schleife in eine andere Variable übertragen.

Einige dieser "Zählungen" können überraschend teuer sein, insbesondere wenn Sie mit Datenbanken arbeiten. Selbst wenn Sie an einem Datensatz arbeiten, der sich während der Iteration der Liste nicht ändern soll.

Pieter B
quelle
Wenn Zählung teuer ist, sollten Sie es überhaupt nicht verwenden. Stattdessen sollten Sie das Äquivalent vonfor( auto it = begin(dataset); !at_end(it); ++it )
Ben Voigt
@BenVoigt Die Verwendung eines Iterators ist definitiv der beste Weg für diese Operationen. Ich habe ihn nur erwähnt, um meinen Standpunkt zur Verwendung von nicht reinen Methoden mit Nebenwirkungen zu veranschaulichen.
Pieter B
0

Meiner Meinung nach ist dies sehr sprachspezifisch. Wenn ich beispielsweise C ++ 11 verwende, würde ich vermuten, dass constexprder Compiler , wenn die Bedingungsprüfung eine Funktion wäre, sehr wahrscheinlich die mehreren Ausführungen optimieren würde, da er weiß, dass sie jedes Mal den gleichen Wert ergeben.

Wenn es sich bei dem Funktionsaufruf jedoch um eine Bibliotheksfunktion handelt, die nicht constexprvom Compiler ausgeführt wird, wird sie mit ziemlicher Sicherheit bei jeder Iteration ausgeführt, da dies nicht abgeleitet werden kann (es sei denn, sie ist inline und kann daher als rein abgeleitet werden).

Ich weiß weniger über Java, aber angesichts der Tatsache, dass JIT kompiliert ist, würde ich vermuten, dass der Compiler zur Laufzeit über genügend Informationen verfügt, um die Bedingung wahrscheinlich inline zu integrieren und zu optimieren. Dies würde jedoch von einem guten Compiler-Design abhängen, und der Compiler, der diese Schleife entschied, war eine Optimierungspriorität, die wir nur erraten können.

Persönlich finde ich es etwas eleganter, die Bedingung in die for-Schleife zu setzen, wenn Sie können, aber wenn sie komplex ist, schreibe ich sie in eine constexproder inline-Funktion oder in Ihre entsprechenden Sprachen, um darauf hinzuweisen, dass die Funktion rein und optimierbar ist. Dies macht die Absicht offensichtlich und behält den idiomatischen Schleifenstil bei, ohne eine riesige unlesbare Linie zu erzeugen. Außerdem erhält die Bedingungsprüfung einen Namen, wenn es sich um eine eigene Funktion handelt, sodass die Leser sofort logisch sehen können, wozu die Prüfung dient, ohne sie zu lesen, wenn sie komplex ist.

Wertigkeit
quelle