Wird der Code
int a = ((1 + 2) + 3); // Easy to read
renn langsamer als
int a = 1 + 2 + 3; // (Barely) Not quite so easy to read
oder sind moderne Compiler clever genug, um "nutzlose" Klammern zu entfernen / optimieren.
Es mag wie ein winziges Optimierungsproblem erscheinen, aber bei der Auswahl von C ++ anstelle von C # / Java / ... dreht sich alles um Optimierungen (IMHO).
c++
optimization
compilation
Serge
quelle
quelle
Antworten:
Der Compiler fügt keine Klammern ein oder entfernt sie nicht. Es wird lediglich ein Analysebaum erstellt (in dem keine Klammern vorhanden sind), der Ihrem Ausdruck entspricht, und dabei müssen die von Ihnen geschriebenen Klammern berücksichtigt werden. Wenn Sie Ihren Ausdruck vollständig in Klammern setzen, wird dem menschlichen Leser auch sofort klar, was dieser Analysebaum ist. Wenn Sie so weit gehen, dass Sie wie in eklatant redundante Klammern einfügen, werden
int a = (((0)));
Sie die Neuronen des Lesers unnötig belasten und gleichzeitig einige Zyklen im Parser verschwenden, ohne jedoch den resultierenden Analysebaum (und damit den generierten Code) zu ändern ) das geringste bisschen.Wenn Sie nicht alle Klammern schreiben, dann muss der Parser immer noch seine Arbeit einen Parse - Baum zu schaffen, und die Regeln für Betreiber Rangfolge und Assoziativität sagen , dass es genau die Parse - Baum es Konstrukt muss. Sie können diese Regeln als Hinweis für den Compiler betrachten, welche (impliziten) Klammern er in Ihren Code einfügen soll , obwohl der Parser in diesem Fall eigentlich nie Klammern behandelt: Er wurde nur so konstruiert, dass er denselben Analysebaum wie Klammern erzeugt waren an bestimmten Orten anwesend. Wenn Sie genau an diesen Stellen Klammern setzen, wie in
int a = (1+2)+3;
(Assoziativität von+
steht links), gelangt der Parser auf einem etwas anderen Weg zum gleichen Ergebnis. Wenn Sie andere Klammern als in einfügenint a = 1+(2+3);
Dann erzwingen Sie einen anderen Analysebaum, wodurch möglicherweise ein anderer Code generiert wird (obwohl dies möglicherweise nicht der Fall ist, da der Compiler nach dem Erstellen des Analysebaums möglicherweise Transformationen anwendet, sofern sich die Auswirkungen der Ausführung des resultierenden Codes niemals ändern würden es). Angenommen, es gibt einen Unterschied im Resuting-Code, so kann im Allgemeinen nichts gesagt werden, was effizienter ist. Der wichtigste Punkt ist natürlich, dass die Analysebäume die meiste Zeit keine mathematisch äquivalenten Ausdrücke liefern. Ein Vergleich der Ausführungsgeschwindigkeit ist daher nebensächlich: Man sollte nur den Ausdruck schreiben, der das richtige Ergebnis liefert.Das Fazit ist also: Verwenden Sie Klammern, wie es für die Korrektheit und für die Lesbarkeit gewünscht ist; Wenn sie redundant sind, haben sie keinerlei Einfluss auf die Ausführungsgeschwindigkeit (und einen vernachlässigbaren Einfluss auf die Kompilierungszeit).
Und nichts davon hat etwas mit der Optimierung zu tun, die mit der Erstellung des Analysebaums einhergeht, sodass nicht bekannt ist, wie der Analysebaum erstellt wurde. Dies gilt unverändert von den ältesten und dümmsten Compilern zu den intelligentesten und modernsten. Nur in einer interpretierten Sprache (wo "Kompilierungszeit" und "Ausführungszeit" zusammenfallen) könnte es möglicherweise eine Strafe für redundante Klammern geben, aber selbst dann denke ich, dass die meisten dieser Sprachen so organisiert sind, dass zumindest die Analysephase nur einmal durchgeführt wird für jede Anweisung (Speichern einer vorab analysierten Form zur Ausführung).
quelle
a = b + c * d;
,a = b + (c * d);
wäre [harmlos] überflüssige Klammern. Wenn sie Ihnen helfen, den Code besser lesbar zu machen, ist das in Ordnung.a = (b + c) * d;
wäre nicht redundante Klammern - sie ändern tatsächlich den resultierenden Analysebaum und geben ein anderes Ergebnis. Es ist vollkommen legal, dies zu tun (tatsächlich, notwendig), aber sie stimmen nicht mit der implizierten Standardgruppierung überein.Die Klammern sind nur für Sie da - nicht für die Compiler. Der Compiler erstellt den richtigen Maschinencode, um Ihre Anweisung darzustellen.
Zu Ihrer Information, der Compiler ist klug genug, um es vollständig zu optimieren, wenn es möglich ist. In Ihren Beispielen wird dies
int a = 6;
zur Kompilierungszeit ausgeführt.quelle
int a = 8;// = 2*3 + 5
int five = 7; //HR made us change this to six...
Die Antwort auf die Frage, die Sie tatsächlich gestellt haben, lautet Nein, aber die Antwort auf die Frage, die Sie stellen wollten, lautet Ja. Das Hinzufügen von Klammern verlangsamt den Code nicht.
Sie haben eine Frage zur Optimierung gestellt, aber Klammern haben nichts mit Optimierung zu tun. Der Compiler wendet eine Vielzahl von Optimierungstechniken an, um entweder die Größe oder die Geschwindigkeit des generierten Codes (manchmal beides) zu verbessern. Beispielsweise könnte der Ausdruck A ^ 2 (A im Quadrat) durch A x A (A multipliziert mit sich selbst) ersetzt werden, wenn dies schneller ist. Die Antwort hier ist nein, der Compiler macht in seiner Optimierungsphase nichts anderes, je nachdem ob Klammern vorhanden sind oder nicht.
Ich denke, Sie wollten fragen, ob der Compiler immer noch denselben Code generiert, wenn Sie einem Ausdruck unnötige Klammern an Stellen hinzufügen, die Ihrer Meinung nach die Lesbarkeit verbessern könnten. Mit anderen Worten, wenn Sie Klammern hinzufügen, ist der Compiler intelligent genug, um sie wieder zu entfernen, anstatt irgendwie schlechteren Code zu generieren. Die Antwort lautet ja, immer.
Lassen Sie mich das vorsichtig sagen. Wenn Sie einem Ausdruck Klammern hinzufügen, die absolut unnötig sind (unabhängig von der Bedeutung oder der Reihenfolge der Auswertung eines Ausdrucks), verwirft der Compiler diese im Hintergrund und generiert denselben Code.
Es gibt jedoch bestimmte Ausdrücke, bei denen scheinbar unnötige Klammern die Reihenfolge der Auswertung eines Ausdrucks tatsächlich ändern. In diesem Fall generiert der Compiler Code, um das, was Sie tatsächlich geschrieben haben, in die Tat umzusetzen, was möglicherweise von dem abweicht, was Sie beabsichtigt haben. Hier ist ein Beispiel. Mach das nicht!
Fügen Sie also Klammern hinzu, wenn Sie möchten, aber stellen Sie sicher, dass sie wirklich nicht erforderlich sind!
Ich mache nie. Es besteht das Risiko eines Fehlers, ohne dass ein wirklicher Nutzen daraus resultiert.
Fußnote: Vorzeichenloses Verhalten tritt auf, wenn ein vorzeichenbehafteter ganzzahliger Ausdruck einen Wert ergibt, der außerhalb des Bereichs liegt, den er ausdrücken kann, in diesem Fall -32767 bis +32767. Dies ist ein komplexes Thema, das für diese Antwort nicht in Frage kommt.
quelle
a
sich wirklich um unsignierte Zahlen handeln kann, kann-a + b
es leicht zu einem Überlauf kommen, wenna
diese negativ oderb
positiv sind.(b+c)
in der letzten Zeile unterstützt die Argumente vonint
. Wenn der Compilerint
nicht 16 Bit definiert (entweder weil er veraltet ist oder auf einen kleinen Mikrocontroller abzielt), ist die letzte Zeile absolut legitim.int
befördert wird, wird befördert, esint
sei denn, dieser Typ kann nicht alle seine Werte darstellen. In diesem Fall wird er befördertunsigned int
. Compiler können die Werbeaktionen weglassen, wenn alle definierten Verhaltensweisen mit denen der Werbeaktionen übereinstimmen . Auf einer Maschine, auf der sich 16-Bit-Typen wie ein umschließender abstrakter algebraischer Ring verhalten, sind (a + b) + c und a + (b + c) gleichwertig. Wennint
es sich jedoch um einen 16-Bit-Typ handelt, der beim ÜberlaufKlammern dienen nur dazu, die Rangfolge der Operatoren zu ändern. Einmal kompilierte Klammern sind nicht mehr vorhanden, da sie zur Laufzeit nicht benötigt werden. Der Kompilierungsprozess entfernt alle Klammern, Leerzeichen und anderen syntaktischen Zuckersymbole, die Sie und ich benötigen, und ändert alle Operatoren in etwas [weit] Einfacheres, das der Computer ausführen kann.
Also, wo Sie und ich vielleicht sehen ...
... ein Compiler könnte etwas ähnliches ausgeben:
Das Programm wird ausgeführt, indem am Anfang begonnen wird und jede Anweisung der Reihe nach ausgeführt wird.
Die Operatorrangfolge lautet jetzt "Wer zuerst kommt, mahlt zuerst".
Alles ist stark typisiert, da der Compiler alle gearbeitet , dass , während es wurde neben die ursprüngliche Syntax zu reißen.
OK, es ist nichts wie das Zeug, mit dem du und ich es zu tun haben, aber dann leiten wir es nicht!
quelle
Kommt darauf an, ob es ein Gleitkomma ist oder nicht:
Bei der Gleitkomma-Arithmetik ist die Addition nicht assoziativ, sodass der Optimierer die Operationen nicht neu anordnen kann (es sei denn, Sie fügen den Fastmath-Compiler-Schalter hinzu).
Bei ganzzahligen Operationen können sie neu angeordnet werden.
In Ihrem Beispiel werden beide zur exakt gleichen Zeit ausgeführt, da sie zum exakt gleichen Code kompiliert werden (die Addition wird von links nach rechts ausgewertet).
Auch wenn Java und C # es optimieren können, tun sie dies nur zur Laufzeit.
quelle
Der typische C ++ - Compiler übersetzt in Maschinencode, nicht in C ++ . Es entfernt nutzlose Parens, ja, denn bis es fertig ist, gibt es überhaupt keine Parens. Maschinencode funktioniert nicht so.
quelle
Beide Codes werden als 6 fest codiert:
Überzeugen Sie sich hier
quelle
Nein, aber ja, aber vielleicht, aber vielleicht andersherum, aber nein.
Wie bereits erwähnt, ist der Ausdruck (unter der Annahme einer Sprache, in der die Addition linksassoziativ ist, z. B. C, C ++, C # oder Java)
((1 + 2) + 3)
genau gleichbedeutend mit1 + 2 + 3
. Es sind verschiedene Arten, etwas in den Quellcode zu schreiben, das keine Auswirkung auf den resultierenden Maschinencode oder Bytecode hätte.In beiden Fällen wird das Ergebnis eine Anweisung sein, z. B. zwei Register und dann ein drittes hinzuzufügen oder zwei Werte von einem Stapel zu nehmen, sie hinzuzufügen, zurückzuschieben, sie und ein anderes zu nehmen und sie hinzuzufügen oder drei Register hinzuzufügen eine einzelne Operation oder eine andere Möglichkeit, drei Zahlen zu summieren, je nachdem, was auf der nächsten Ebene am sinnvollsten ist (Maschinencode oder Bytecode). Im Fall von Byte-Code wird dieser wiederum wahrscheinlich eine ähnliche Umstrukturierung erfahren, z. B. das IL-Äquivalent davon (das wäre eine Reihe von Ladevorgängen für einen Stapel, und Popping-Paare zum Hinzufügen und anschließenden Zurückschieben des Ergebnisses). Dies würde nicht zu einer direkten Kopie dieser Logik auf Maschinencodeebene führen, sondern zu etwas Sinnvollerem für die betreffende Maschine.
Aber Ihre Frage hat noch etwas mehr zu bieten.
Im Fall eines vernünftigen C-, C ++ -, Java- oder C # -Compilers würde ich erwarten, dass das Ergebnis der beiden von Ihnen angegebenen Anweisungen genau die gleichen Ergebnisse liefert wie:
Warum sollte der resultierende Code Zeit mit der Berechnung von Literalen verschwenden? Keine Änderungen am Status des Programms verhindern das Ergebnis des
1 + 2 + 3
Seins. Daher6
sollte dies im ausgeführten Code berücksichtigt werden. In der Tat, vielleicht nicht einmal das (je nachdem, was Sie mit diesen 6 machen, können wir das Ganze vielleicht wegwerfen; und selbst C # mit der Philosophie "Optimiere nicht stark, da der Jitter dies sowieso optimieren wird" wird beides produzieren das Äquivalent von (int a = 6
oder wirf das Ganze einfach als unnötig weg).Dies führt uns jedoch zu einer möglichen Erweiterung Ihrer Frage. Folgendes berücksichtigen:
und
(Beachten Sie, dass die letzten beiden Beispiele kein gültiges C # sind, während alles andere hier ist und sie in C, C ++ und Java gültig sind.)
Auch hier haben wir genau den gleichen Code in Bezug auf die Ausgabe. Da es sich nicht um konstante Ausdrücke handelt, werden sie beim Kompilieren nicht berechnet. Möglicherweise ist eine Form schneller als eine andere. Welche ist schneller? Das hängt vom Prozessor und möglicherweise von einigen eher willkürlichen Zustandsunterschieden ab (zumal wenn man schneller ist, ist es wahrscheinlich nicht viel schneller).
Und sie haben nichts mit Ihrer Frage zu tun , da es hauptsächlich um Unterschiede in der Reihenfolge geht, in der etwas konzeptionell getan wird.
In jedem von ihnen gibt es einen Grund zu der Annahme, dass einer schneller ist als der andere. Einzelne Dekremente können eine spezielle Anweisung haben und
(b / 2)--
könnten in der Tat schneller sein als(b - 2) / 2
.d * 32
könnte vielleicht schneller produziert werden, indem man esd << 5
sod * 32 - d
schneller macht alsd * 31
. Die Unterschiede zwischen den letzten beiden sind besonders interessant; In einem Fall kann ein Teil der Verarbeitung übersprungen werden, im anderen Fall wird jedoch die Möglichkeit einer Verzweigungsfehlvorhersage vermieden.Wir haben also zwei Fragen: 1. Ist eine tatsächlich schneller als die andere? 2. Wandelt ein Compiler das langsamere in das schnellere um?
Und die Antwort ist 1. Es kommt darauf an. 2. Vielleicht.
Oder zu erweitern, es kommt darauf an, weil es auf den jeweiligen Prozessor ankommt. Sicherlich gab es Prozessoren, bei denen das naive Maschinencode-Äquivalent des einen schneller war als das naive Maschinencode-Äquivalent des anderen. Im Laufe der Geschichte des elektronischen Rechnens hat es auch keinen gegeben, der immer schneller war (insbesondere das Element der Verzweigungsfehlvorhersage war für viele nicht relevant, als CPUs ohne Pipeline häufiger auftraten).
Und vielleicht, weil es eine Reihe verschiedener Optimierungen gibt, die Compiler (und Jitter und Skript-Engines) durchführen, und obwohl einige in bestimmten Fällen vorgeschrieben sind, können wir im Allgemeinen einige logisch äquivalente Code-Teile finden, die diesen Anforderungen entsprechen Selbst der naivste Compiler hat genau die gleichen Ergebnisse und einige logisch äquivalente Code-Teile, bei denen selbst der raffinierteste Code für den einen Code schneller als für den anderen erzeugt (selbst wenn wir etwas völlig Pathologisches schreiben müssen, um unseren Standpunkt zu beweisen).
Nein. Selbst mit komplizierteren Unterschieden als denen, die ich hier gebe, scheint es eine absolut winzige Angelegenheit zu sein, die nichts mit Optimierung zu tun hat. Wenn überhaupt, ist es eine Frage der Pessimisierung, da Sie den Verdacht haben, dass das schwer zu lesende
((1 + 2) + 3
langsamer sein könnte als das leichter zu lesende1 + 2 + 3
.Wenn es wirklich darum geht, C ++ gegenüber C # oder Java zu wählen, sollten die Leute ihre Kopie von Stroustrup und ISO / IEC 14882 brennen und den Speicherplatz ihres C ++ - Compilers freigeben, um Platz für weitere MP3s oder ähnliches zu lassen.
Diese Sprachen haben unterschiedliche Vorteile.
Eine davon ist, dass C ++ im Allgemeinen immer noch schneller und speicherfreundlicher ist. Ja, es gibt Beispiele, in denen C # und / oder Java schneller sind und / oder eine bessere Nutzung des Arbeitsspeichers während der gesamten Anwendungslebensdauer aufweisen. Diese werden immer häufiger, je besser die beteiligten Technologien sind Eine kleinere ausführbare Datei, die ihre Arbeit schneller erledigt und weniger Speicher benötigt als die entsprechende Datei in einer dieser beiden Sprachen.
Dies ist keine Optimierung.
Mit Optimierung wird manchmal gemeint, dass die Dinge schneller gehen. Das ist verständlich, denn oft, wenn wir wirklich von "Optimierung" sprechen, geht es tatsächlich darum, die Dinge schneller zu machen, und so ist eine Abkürzung für die andere geworden, und ich gebe zu, dass ich das Wort selbst so falsch verwende.
Das richtige Wort für "Dinge schneller machen" ist nicht Optimierung . Das richtige Wort ist hier Verbesserung . Wenn Sie ein Programm ändern und der einzige bedeutende Unterschied darin besteht, dass es jetzt schneller ist, es in keiner Weise optimiert ist, sondern nur besser.
Optimierung ist, wenn wir eine Verbesserung in Bezug auf einen bestimmten Aspekt und / oder einen bestimmten Fall vornehmen. Häufige Beispiele sind:
Solche Fälle wären gerechtfertigt, wenn zB:
Solche Fälle wären jedoch auch in anderen Szenarien nicht gerechtfertigt: Der Code wurde nicht durch ein absolut unfehlbares Qualitätsmaß verbessert, sondern in einer bestimmten Hinsicht, die ihn für eine bestimmte Verwendung geeigneter macht. optimiert.
Die Wahl der Sprache hat hier Auswirkungen, da sowohl die Geschwindigkeit, die Speichernutzung und die Lesbarkeit als auch die Kompatibilität mit anderen Systemen, die Verfügbarkeit von Bibliotheken, die Verfügbarkeit von Laufzeiten und die Laufzeitreife auf einem bestimmten Betriebssystem davon betroffen sein können (Für meine Sünden hatte ich irgendwie Linux und Android als meine Lieblingsbetriebssysteme und C # als meine Lieblingssprache, und während Mono großartig ist, stoße ich immer noch ziemlich darauf).
Die Aussage "C ++ anstelle von C # / Java / ... zu wählen, bedeutet nur, dass es sich um Optimierungen handelt" ist sinnvoll, wenn Sie der Meinung sind, dass C ++ wirklich scheiße ist, denn bei der Optimierung geht es um "besser trotz ..." und nicht um "besser". Wenn Sie der Meinung sind, dass C ++ trotz allem besser ist, müssen Sie sich als letztes Gedanken über solche winzigen möglichen Micro-Opts machen. In der Tat ist es wahrscheinlich besser, wenn Sie es überhaupt aufgeben. Happy Hacker sind eine Qualität, für die es sich zu optimieren lohnt!
Wenn Sie jedoch dazu neigen zu sagen, "Ich liebe C ++, und eines der Dinge, die ich daran liebe, ist das Herausdrücken zusätzlicher Zyklen", dann ist das eine andere Sache. Es ist immer noch so, dass sich Mikro-Opts nur dann lohnen, wenn sie eine reflexive Angewohnheit sind (das heißt, die Art und Weise, wie Sie dazu neigen, natürlich zu codieren, ist umso schneller, je langsamer sie sind). Ansonsten handelt es sich nicht einmal um eine vorzeitige Optimierung, sondern um eine vorzeitige Pessimisierung, die die Situation nur verschlimmert.
quelle
Klammern sollen dem Compiler mitteilen, in welcher Reihenfolge Ausdrücke ausgewertet werden sollen. Manchmal sind sie nutzlos (außer sie verbessern oder verschlechtern die Lesbarkeit), weil sie die Reihenfolge angeben, in der sie sowieso verwendet werden. Manchmal ändern sie die Reihenfolge. Im
Praktisch jede existierende Sprache hat die Regel, dass die Summe ausgewertet wird, indem man 1 + 2 addiert und dann das Ergebnis plus 3 addiert. Wenn Sie geschrieben haben
dann würde die Klammer eine andere Reihenfolge erzwingen: Zuerst 2 + 3 addieren, dann 1 plus das Ergebnis addieren. Ihr Beispiel in Klammern erzeugt die gleiche Reihenfolge, die ohnehin erzeugt worden wäre. In diesem Beispiel ist die Reihenfolge der Operationen etwas anders, aber die Funktionsweise der Ganzzahladdition ist dieselbe. Im
die Klammern sind kritisch; Lässt man sie weg, ändert sich das Ergebnis von 9 auf 1.
Nachdem der Compiler bestimmt hat, welche Operationen in welcher Reihenfolge ausgeführt werden, werden die Klammern vollständig vergessen. Der Compiler merkt sich zu diesem Zeitpunkt nur, welche Operationen in welcher Reihenfolge ausgeführt werden sollen. Es gibt also eigentlich nichts, was der Compiler hier optimieren könnte, die Klammern sind weg .
quelle
practically every language in existence
; außer APL: Versuchen Sie (hier) [tryapl.org],(1-2)+3
(2),1-(2+3)
(-4) und1-2+3
(auch -4) einzugeben .Ich stimme mit vielem überein, was gesagt wurde, aber ... das Übergewicht hier ist, dass die Klammern da sind, um die Reihenfolge der Operationen zu erzwingen ... was der Compiler absolut tut. Ja, es erzeugt Maschinencode… aber das ist nicht der Punkt und wird nicht gefragt.
Die Klammern sind in der Tat weg: Wie gesagt, sie sind nicht Teil des Maschinencodes, der Zahlen und nichts anderes ist. Der Assembler-Code ist kein Maschinencode, er ist halbmenschlich lesbar und enthält die Anweisungen namentlich - nicht den Opcode. Die Maschine führt sogenannte Opcodes aus - numerische Darstellungen der Assemblersprache.
Sprachen wie Java fallen in einen Zwischenbereich, da sie nur teilweise auf dem Computer kompiliert werden, auf dem sie erstellt werden. Sie werden kompiliert, um auf dem Computer, auf dem sie ausgeführt werden, maschinenspezifischen Code zu erstellen. Dies macht jedoch keinen Unterschied - die Klammern sind nach der ersten Kompilierung immer noch weg.
quelle
a = f() + (g() + h());
den Compiler frei zu nennenf
,g
undh
in dieser Reihenfolge (oder in beliebiger Reihenfolge gefällt ihm).Compiler übersetzen, unabhängig von der Sprache, alle Infix-Mathematik in Postfix. Mit anderen Worten, wenn der Compiler etwas sieht wie:
es übersetzt es in das:
Dies geschieht, weil die Infix-Notation für die Benutzer einfacher zu lesen ist, die Postfix-Notation jedoch den tatsächlichen Schritten, die der Computer ausführen muss, um die Aufgabe zu erledigen, sehr viel näher kommt (und weil es dafür bereits einen gut entwickelten Algorithmus gibt.) Definition: Postfix beseitigt alle Probleme mit der Reihenfolge der Operationen oder runden Klammern, was die Arbeit beim eigentlichen Schreiben des Maschinencodes natürlich erheblich erleichtert.
Ich empfehle den Wikipedia-Artikel zur umgekehrten polnischen Notation, um weitere Informationen zu diesem Thema zu erhalten.
quelle