Dies ist eine Situation, der ich als unerfahrener Programmierer häufig begegne und die ich mich besonders für ein ehrgeiziges, geschwindigkeitsintensives Projekt wundere, das ich zu optimieren versuche. Werden diese beiden Funktionen für die wichtigsten C-ähnlichen Sprachen (C, objC, C ++, Java, C # usw.) und ihre üblichen Compiler genauso effizient ausgeführt? Gibt es einen Unterschied im kompilierten Code?
void foo1(bool flag)
{
if (flag)
{
//Do stuff
return;
}
//Do different stuff
}
void foo2(bool flag)
{
if (flag)
{
//Do stuff
}
else
{
//Do different stuff
}
}
Grundsätzlich gibt es jemals einen direkten Effizienzbonus / eine direkte Effizienzstrafe, wenn Sie früh break
oder return
früh gehen? Wie ist der Stackframe beteiligt? Gibt es optimierte Sonderfälle? Gibt es Faktoren (wie Inlining oder die Größe von "Do stuff"), die dies erheblich beeinflussen könnten?
Ich bin immer ein Befürworter einer verbesserten Lesbarkeit gegenüber kleineren Optimierungen (ich sehe foo1 häufig bei der Parametervalidierung), aber dies kommt so häufig vor, dass ich alle Sorgen ein für alle Mal beiseite lassen möchte.
Und ich bin mir der Fallstricke einer vorzeitigen Optimierung bewusst ... ugh, das sind einige schmerzhafte Erinnerungen.
EDIT: Ich habe eine Antwort akzeptiert, aber die Antwort von EJP erklärt ziemlich prägnant, warum die Verwendung von a return
praktisch vernachlässigbar ist (bei der Montage return
wird eine 'Verzweigung' bis zum Ende der Funktion erstellt, was extrem schnell ist. Die Verzweigung verändert das PC-Register und Dies kann sich auch auf den Cache und die Pipeline auswirken, was ziemlich winzig ist.) Insbesondere in diesem Fall spielt dies buchstäblich keine Rolle, da sowohl der if/else
als auch return
derselbe Zweig bis zum Ende der Funktion erstellt werden.
Antworten:
Es gibt überhaupt keinen Unterschied:
Dies bedeutet, dass auch ohne Optimierung in zwei Compilern kein Unterschied im generierten Code besteht
quelle
something()
dass immer ausgeführt wird. In der ursprünglichen Frage hat OPDo stuff
undDo diffferent stuff
abhängig von der Flagge. Ich bin nicht sicher, ob der generierte Code der gleiche sein wird.Die kurze Antwort lautet: Kein Unterschied. Tun Sie sich selbst einen Gefallen und machen Sie sich keine Sorgen mehr. Der optimierende Compiler ist fast immer schlauer als Sie.
Konzentrieren Sie sich auf Lesbarkeit und Wartbarkeit.
Wenn Sie sehen möchten, was passiert, bauen Sie diese mit Optimierungen auf und sehen Sie sich die Assembler-Ausgabe an.
quelle
x = <some number>
als nichtif(<would've changed>) x = <some number>
benötigte Zweige wirklich weh tun können ? Auf der anderen Seite würde ich mir darüber auch keine Sorgen machen, es sei denn, dies befindet sich in der Hauptschleife einer extrem intensiven Operation.Interessante Antworten: Obwohl ich (bisher) allen zustimme, gibt es mögliche Konnotationen zu dieser Frage, die bisher völlig außer Acht gelassen wurden.
Wenn das obige einfache Beispiel um die Ressourcenzuweisung und die Fehlerprüfung mit einer möglichen daraus resultierenden Ressourcenfreigabe erweitert wird, kann sich das Bild ändern.
Betrachten Sie den naiven Ansatz, den Anfänger verfolgen könnten:
Das Obige würde eine extreme Version des Stils der vorzeitigen Rückkehr darstellen. Beachten Sie, dass sich der Code im Laufe der Zeit sehr wiederholt und nicht mehr gewartet werden kann, wenn seine Komplexität zunimmt. Heutzutage könnten Leute die Ausnahmebehandlung verwenden , um diese zu fangen.
Philip schlug vor, nachdem er sich das folgende Beispiel angesehen zu haben, einen unterbrechungsfreien Schalter / Koffer im oberen Fangblock zu verwenden. Man könnte wechseln (typeof (e)) und dann durch die
free_resourcex()
Anrufe fallen, aber dies ist nicht trivial und erfordert Designüberlegungen . Und denken Sie daran, dass ein Schalter / Gehäuse ohne Unterbrechungen genau wie das Goto mit verketteten Etiketten unten ist ...Wie Mark B hervorhob, wird es in C ++ als guter Stil angesehen, dem Prinzip der Ressourcenerfassung als Initialisierung zu folgen , kurz RAII . Der Kern des Konzepts besteht darin, die Objektinstanziierung zu verwenden, um Ressourcen zu erwerben. Die Ressourcen werden dann automatisch freigegeben, sobald die Objekte den Gültigkeitsbereich verlassen und ihre Destruktoren aufgerufen werden. Bei voneinander abhängigen Ressourcen muss besonders darauf geachtet werden, die richtige Reihenfolge der Freigabe sicherzustellen und die Objekttypen so zu gestalten, dass die erforderlichen Daten für alle Destruktoren verfügbar sind.
Oder in Tagen vor der Ausnahme könnte Folgendes passieren:
Dieses stark vereinfachte Beispiel weist jedoch mehrere Nachteile auf: Es kann nur verwendet werden, wenn die zugewiesenen Ressourcen nicht voneinander abhängen (z. B. kann es nicht zum Zuweisen von Speicher, zum Öffnen eines Dateihandles und zum Einlesen von Daten aus dem Handle in den Speicher verwendet werden ), und es werden keine einzelnen, unterscheidbaren Fehlercodes als Rückgabewerte bereitgestellt.
Um den Code schnell (!), Kompakt, leicht lesbar und erweiterbar zu halten, hat Linus Torvalds einen anderen Stil für Kernel-Code erzwungen, der sich mit Ressourcen befasst, selbst wenn das berüchtigte goto auf eine absolut sinnvolle Weise verwendet wird :
Der Kern der Diskussion über die Kernel-Mailinglisten ist, dass die meisten Sprachfunktionen, die der goto-Anweisung "vorgezogen" werden, implizite gotos sind, wie z. B. riesige, baumartige if / else-Ausnahmebehandlungsroutinen, Schleifen- / Unterbrechungs- / Fortsetzungsanweisungen usw. Und gotos im obigen Beispiel gelten als in Ordnung, da sie nur eine kleine Strecke springen, klare Beschriftungen haben und den Code anderer Unordnung freigeben, um die Fehlerbedingungen zu verfolgen. Diese Frage wurde auch hier zum Stackoverflow diskutiert .
Was jedoch im letzten Beispiel fehlt, ist eine gute Möglichkeit, einen Fehlercode zurückzugeben. Ich dachte daran,
result_code++
nach jedemfree_resource_x()
Aufruf ein hinzuzufügen und diesen Code zurückzugeben, aber dies gleicht einige der Geschwindigkeitsgewinne des obigen Codierungsstils aus. Und es ist schwierig, im Erfolgsfall 0 zurückzugeben. Vielleicht bin ich nur einfallslos ;-)Ja, ich glaube, es gibt einen großen Unterschied in der Frage, ob vorzeitige Rückgaben codiert werden sollen oder nicht. Ich denke aber auch, dass es nur in komplizierterem Code offensichtlich ist, dass es schwieriger oder unmöglich ist, den Compiler neu zu strukturieren und zu optimieren. Dies ist normalerweise der Fall, sobald die Ressourcenzuweisung ins Spiel kommt.
quelle
catch
Unterbrechungswitch
zum Fehlercode?Auch wenn dies keine gute Antwort ist, kann ein Produktions-Compiler viel besser optimieren als Sie. Ich würde Lesbarkeit und Wartbarkeit gegenüber solchen Optimierungen bevorzugen.
quelle
Um genau zu sein,
return
wird das zu einem Zweig bis zum Ende der Methode kompiliert, wo es eineRET
Anweisung gibt oder was auch immer es sein mag. Wenn Sie es weglassen, wird das Ende des Blocks vor demelse
zu einem Zweig bis zum Ende deselse
Blocks kompiliert . Sie können also in diesem speziellen Fall sehen, dass es überhaupt keinen Unterschied macht.quelle
Wenn Sie wirklich wissen möchten, ob es einen Unterschied im kompilierten Code für Ihren speziellen Compiler und Ihr System gibt, müssen Sie die Assembly selbst kompilieren und betrachten.
Im großen Rahmen ist es jedoch fast sicher, dass der Compiler besser optimieren kann als Ihre Feinabstimmung, und selbst wenn dies nicht möglich ist, ist es sehr unwahrscheinlich, dass dies tatsächlich für die Leistung Ihres Programms von Bedeutung ist.
Schreiben Sie stattdessen den Code so, dass der Mensch ihn am besten lesen und warten kann, und lassen Sie den Compiler das tun, was er am besten kann: Generieren Sie die bestmögliche Assembly aus Ihrer Quelle.
quelle
In Ihrem Beispiel ist die Rendite spürbar. Was passiert mit der Person, die debuggt, wenn die Rückgabe eine oder zwei Seiten über / unter ist, auf denen // verschiedene Dinge auftreten? Viel schwieriger zu finden / zu sehen, wenn mehr Code vorhanden ist.
quelle
Ich stimme Blueshift sehr zu: Lesbarkeit und Wartbarkeit zuerst!. Aber wenn Sie wirklich besorgt sind (oder einfach nur erfahren möchten, was Ihr Compiler tut, was auf lange Sicht definitiv eine gute Idee ist), sollten Sie selbst nachsehen.
Dies bedeutet, dass Sie einen Dekompiler verwenden oder die Compilerausgabe auf niedriger Ebene betrachten (z. B. Assemblersprache). In C # oder einer beliebigen .NET-Sprache bieten Ihnen die hier dokumentierten Tools das, was Sie benötigen.
Aber wie Sie selbst beobachtet haben, ist dies wahrscheinlich eine vorzeitige Optimierung.
quelle
Aus sauberem Code: Ein Handbuch für agile Software-Handwerkskunst
im Code wird den Leser nur dazu bringen, zur Funktion zu navigieren und Zeit damit zu verschwenden, foo zu lesen (boolesches Flag)
Eine besser strukturierte Codebasis bietet Ihnen eine bessere Möglichkeit, den Code zu optimieren.
quelle
Eine Denkrichtung (ich kann mich nicht an den Eierkopf erinnern, der sie im Moment vorgeschlagen hat) ist, dass alle Funktionen aus struktureller Sicht nur einen Rückgabepunkt haben sollten, damit der Code leichter zu lesen und zu debuggen ist. Das ist wohl eher für die Programmierung religiöser Debatten gedacht.
Ein technischer Grund, warum Sie möglicherweise steuern möchten, wann und wie eine Funktion beendet wird, die gegen diese Regel verstößt, besteht darin, dass Sie Echtzeitanwendungen codieren und sicherstellen möchten, dass alle Steuerpfade durch die Funktion dieselbe Anzahl von Taktzyklen benötigen.
quelle
Ich bin froh, dass Sie diese Frage aufgeworfen haben. Sie sollten die Filialen immer über eine frühzeitige Rückgabe verwenden. Warum dort aufhören? Führen Sie alle Ihre Funktionen zu einer zusammen, wenn Sie können (mindestens so viel wie möglich). Dies ist möglich, wenn keine Rekursion vorliegt. Am Ende werden Sie eine massive Hauptfunktion haben, aber genau das brauchen / wollen Sie für diese Art von Dingen. Benennen Sie anschließend Ihre Bezeichner so kurz wie möglich um. Auf diese Weise wird beim Ausführen Ihres Codes weniger Zeit für das Lesen von Namen aufgewendet. Weiter machen ...
quelle