Entfernt / optimiert der C ++ - Compiler unnötige Klammern?

19

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).

Serge
quelle
9
Ich denke, C # und Java werden das auch optimieren. Ich glaube, wenn sie AST analysieren und erstellen, werden sie offensichtlich nutzloses Zeug wie das entfernen.
Farid Nouri Neshat
5
Alles, was ich gelesen habe, verweist auf die JIT-Kompilierung, die es leicht macht, der Kompilierung im Vorfeld eine Chance zu geben, so dass dies allein kein sehr zwingendes Argument ist. Sie rufen die Spielprogrammierung auf - der wahre Grund für die Bevorzugung der Kompilierung im Voraus ist, dass sie vorhersehbar ist - bei der JIT-Kompilierung wissen Sie nie, wann der Compiler einspringen und versuchen wird, mit dem Kompilieren von Code zu beginnen. Ich möchte jedoch darauf hinweisen, dass sich die Kompilierung von Garbage Collection im Voraus in nativem Code nicht gegenseitig ausschließt, siehe z. B. Standard ML und D. Und ich habe überzeugende Argumente dafür gesehen, dass Garbage Collection effizienter ist ...
Doval,
7
... als RAII und Smart Pointer, so ist es eher eine Frage des ausgetretenen Pfades (C ++) als des relativ ungelesenen Pfades, in diesen Sprachen Spiele zu programmieren. Ich würde auch bemerken, dass die Sorge um Klammern verrückt ist - ich verstehe, woher Sie kommen, aber das ist eine lächerliche Mikrooptimierung. Die Auswahl der Datenstrukturen und Algorithmen in Ihrem Programm wird auf jeden Fall die Leistung dominieren, nicht solche Trivialitäten.
Doval
6
Um ... Welche Art von Optimierung erwarten Sie genau? Wenn Sie über statische Analysen sprechen, wird dies in den meisten mir bekannten Sprachen durch das statisch bekannte Ergebnis ersetzt (LLVM-basierte Implementierungen erzwingen dies sogar, AFAIK). Wenn es um die Ausführungsreihenfolge geht, spielt dies keine Rolle, da es sich um denselben Vorgang handelt und keine Nebenwirkungen auftreten. Addition benötigt sowieso zwei Operanden. Wenn Sie dies verwenden, um C ++, Java und C # in Bezug auf die Leistung zu vergleichen, scheint es, als hätten Sie keine klare Vorstellung davon, was Optimierungen sind und wie sie funktionieren. Sie sollten sich stattdessen darauf konzentrieren, dies zu lernen.
Theodoros Chatzigiannakis
5
Ich frage mich, warum a) Sie den Ausdruck in Klammern für lesbarer halten (für mich sieht er nur hässlich und irreführend aus (warum betonen sie diese bestimmte Reihenfolge? Sollte er hier nicht zutreffend sein?) Und klobig) b) warum Sie ohne denken würden Klammern, in denen es möglicherweise besser abschneidet (Parsen ist für eine Maschine eindeutig einfacher , als sich Gedanken über Bedienerprobleme machen zu müssen. Marc van Leuwen sagt jedoch, dass dies keinerlei Einfluss auf die Laufzeit hat).
linksum

Antworten:

87

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).

Marc van Leeuwen
quelle
s / oldes / älteste /. Gute Antwort, +1.
David Conrad
23
Totale Nitpick-Warnung: "Die Frage ist nicht sehr gut gestellt." - Ich stimme dem nicht zu. Unter "gut gestellt" verstehe ich "eindeutig, welche Wissenslücke der Fragesteller füllen möchte". Im Wesentlichen lautet die Frage: "Optimieren für X, A oder B auswählen und warum? Was passiert darunter?", Was zumindest für mich sehr deutlich darauf hindeutet, wie groß die Wissenslücke ist. Der Fehler in der Frage, auf den Sie zu Recht hinweisen und den Sie sehr gut ansprechen, besteht darin, dass sie auf einem fehlerhaften mentalen Modell beruht.
Jonas Kölker
Für 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.
Phil Perry
1
@OrangeDog wahr, es ist eine Schande, dass Bens Kommentar ein paar Leute hervorgebracht hat, die behaupten, VMs seien schneller als native.
gbjbaanb
1
@ JonasKölker: Mein Anfangssatz bezieht sich eigentlich auf die im Titel formulierte Frage: Eine Frage, ob der Compiler Klammern einfügt oder entfernt, kann man nicht wirklich beantworten, da dies auf einem Missverständnis der Funktionsweise von Compilern beruht. Ich stimme jedoch zu, dass es ziemlich klar ist, welche Wissenslücke behoben werden muss.
Marc van Leeuwen
46

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.

gbjbaanb
quelle
9
Absolut - setzen Sie so viele Klammern ein, wie Sie möchten, und lassen Sie den Compiler die harte Arbeit erledigen, um herauszufinden, was Sie wollen :)
gbjbaanb
23
Bei @Serge True Programming geht es mehr um die Lesbarkeit Ihres Codes als um die Leistung. Sie werden sich nächstes Jahr hassen, wenn Sie einen Absturz debuggen müssen und nur "optimierten" Code haben.
Ratschenfreak
1
@ratchetfreak, du hast recht, aber ich weiß auch, wie ich meinen Code kommentieren kann. int a = 6; // = (1 + 2) + 3
Serge
20
@Serge Ich weiß , das wird nicht nach einem Jahr zwickt halten, in der Zeit Kommentaren und Code synchron gehen und Sie dann am Ende mitint a = 8;// = 2*3 + 5
Ratsche Freak
21
oder von www.thedailywtf.com:int five = 7; //HR made us change this to six...
Mooing Duck
23

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!

short int a = 30001, b = 30002, c = 30003;
int d = -a + b + c;    // ok
int d = (-a + b) + c;  // ok, same code
int d = (-a + b + c);  // ok, same code
int d = ((((-a + b)) + c));  // ok, same code
int d = -a + (b + c);  // undefined behaviour, different code

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.

david.pfx
quelle
Undefiniertes Verhalten in der letzten Zeile liegt daran, dass ein vorzeichenbehafteter Short nur 15 Bit nach dem Vorzeichen enthält, also eine maximale Größe von 32767, richtig? In diesem einfachen Beispiel sollte der Compiler vor einem Überlauf warnen, oder? +1 für ein Gegenbeispiel, egal wie. Wenn sie Parameter für eine Funktion wären, würden Sie keine Warnung erhalten. Auch wenn es asich wirklich um unsignierte Zahlen handeln kann, kann -a + bes leicht zu einem Überlauf kommen, wenn adiese negativ oder bpositiv sind.
Patrick M
@PatrickM: siehe bearbeiten. Undefiniertes Verhalten bedeutet, dass der Compiler tun kann, was er möchte, einschließlich der Ausgabe einer Warnung oder nicht. Arithmetik ohne Vorzeichen erzeugt kein UB, sondern wird modulo um die nächsthöhere Zweierpotenz reduziert.
david.pfx
Der Ausdruck (b+c)in der letzten Zeile unterstützt die Argumente von int. Wenn der Compiler intnicht 16 Bit definiert (entweder weil er veraltet ist oder auf einen kleinen Mikrocontroller abzielt), ist die letzte Zeile absolut legitim.
Superkatze
@supercat: Das glaube ich nicht. Der gebräuchliche Typ und Typ des Ergebnisses sollte short int sein. Wenn das noch nie gefragt wurde, möchten Sie vielleicht eine Frage stellen?
david.pfx
@ david.pfx: Die Regeln für C-arithmetische Beförderungen sind ziemlich klar: Alles, was kleiner als intbefördert wird, wird befördert, es intsei denn, dieser Typ kann nicht alle seine Werte darstellen. In diesem Fall wird er befördert unsigned 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. Wenn intes sich jedoch um einen 16-Bit-Typ handelt, der beim Überlauf
abgefangen wird
7

Klammern 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 ...

  • int a = ((1 + 2) + 3);

... ein Compiler könnte etwas ähnliches ausgeben:

  • Char [1] :: "a"
  • Int32 :: DeclareStackVariable ()
  • Int32 :: 0x00000001
  • Int32 :: 0x00000002
  • Int32 :: Add ()
  • Int32 :: 0x00000003
  • Int32 :: Add ()
  • Int32 :: AssignToVariable ()
  • void :: DiscardResult ()

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!

Phill W.
quelle
4
Es gibt keinen einzigen C ++ - Compiler, der auch nur aus der Ferne so etwas erzeugt. Im Allgemeinen erzeugen sie tatsächlichen CPU-Code, und die Assembly sieht auch nicht so aus.
MSalters
3
hier sollte der strukturelle Unterschied zwischen dem geschriebenen Code und der kompilierten Ausgabe demonstriert werden. Selbst hier könnten die meisten Leute den Maschinencode oder die Baugruppe nicht lesen
DHall
@MSalters Nitpicking Clang würde "so etwas" ausgeben, wenn Sie LLVM ISA als "so etwas" behandeln (SSA ist nicht stapelbasiert). Da es möglich ist, JVM-Backend für LLVM und JVM ISA (AFAIK) zu schreiben, würde Stack-basierter Clang-> llvm-> JVM sehr ähnlich aussehen.
Maciej Piechotka
Ich glaube nicht, dass die LLVM ISA die Möglichkeit hat, Namen-Stack-Variablen mithilfe von Laufzeit-String-Literalen zu definieren (nur die ersten beiden Anweisungen). Dies ist eine ernsthafte Vermischung von Laufzeit und Kompilierungszeit. Die Unterscheidung ist wichtig, weil es bei dieser Frage genau um diese Verwirrung geht.
MSalters
6

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.

Ratschenfreak
quelle
+1 für das Hervorheben, dass Gleitkommaoperationen nicht assoziativ sind.
Doval
Im Beispiel der Frage ändern die Klammern nicht die Standardassoziativität (links), daher ist dieser Punkt umstritten.
Marc van Leeuwen
1
Über den letzten Satz denke ich nicht. Sowohl in Java A als auch in C # erzeugt der Compiler optimierten Bytecode / IL. Die Laufzeit ist nicht betroffen.
Stefano Altieri
IL arbeitet nicht mit Ausdrücken dieser Art, die Anweisungen nehmen eine bestimmte Anzahl von Werten von einem Stapel und geben eine bestimmte Anzahl von Werten (im Allgemeinen 0 oder 1) an den Stapel zurück. Es ist Unsinn, davon zu sprechen, dass so etwas zur Laufzeit in C # optimiert wird.
Jon Hanna
6

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.

Der Löffeligste
quelle
5

Beide Codes werden als 6 fest codiert:

movl    $6, -4(%rbp)

Überzeugen Sie sich hier

MonoThreaded
quelle
2
Was ist das für ein Assembly.ynh.io- Ding?
Peter Mortensen
Es ist ein Pseudo-Compiler, der C-Code in x86-Assemblys online
umwandelt
1

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 mit 1 + 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:

int a = 6;

Warum sollte der resultierende Code Zeit mit der Berechnung von Literalen verschwenden? Keine Änderungen am Status des Programms verhindern das Ergebnis des 1 + 2 + 3Seins. Daher 6sollte 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 = 6oder wirf das Ganze einfach als unnötig weg).

Dies führt uns jedoch zu einer möglichen Erweiterung Ihrer Frage. Folgendes berücksichtigen:

int a = (b - 2) / 2;
/* or */
int a = (b / 2)--;

und

int c;
if(d < 100)
  c = 0;
else
  c = d * 31;
/* or */
int c = d < 100 ? 0 : d * 32 - d
/* or */
int c = d < 100 && d * 32 - d;
/* or */
int c = (d < 100) * (d * 32 - d);

(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 * 32könnte vielleicht schneller produziert werden, indem man es d << 5so d * 32 - dschneller macht als d * 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).

Es scheint vielleicht ein sehr kleines Optimierungsproblem zu sein,

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) + 3langsamer sein könnte als das leichter zu lesende 1 + 2 + 3.

Bei der Auswahl von C ++ anstelle von C # / Java / ... geht es jedoch nur um Optimierungen (IMHO).

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:

  1. In einem Anwendungsfall ist es jetzt schneller, in einem anderen langsamer.
  2. Es ist jetzt schneller, verbraucht aber mehr Speicher.
  3. Das Gedächtnis ist jetzt leichter, aber langsamer.
  4. Es ist jetzt schneller, aber schwieriger zu warten.
  5. Es ist jetzt einfacher zu warten, aber langsamer.

Solche Fälle wären gerechtfertigt, wenn zB:

  1. Der schnellere Anwendungsfall ist anfangs häufiger oder schwerwiegender beeinträchtigt.
  2. Das Programm war inakzeptabel langsam und wir haben viel RAM frei.
  3. Das Programm kam zum Stillstand, weil es so viel RAM verbrauchte, dass mehr Zeit für das Austauschen aufgewendet wurde als für die Ausführung seiner superschnellen Verarbeitung.
  4. Das Programm war inakzeptabel langsam, und der schwer zu verstehende Code ist gut dokumentiert und relativ stabil.
  5. Das Programm ist immer noch akzeptabel schnell, und die verständlichere Codebasis ist billiger zu warten und ermöglicht es, andere Verbesserungen leichter vorzunehmen.

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.

Jon Hanna
quelle
0

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

int a = 1 + 2 + 3;

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

int a = 1 + (2 + 3);

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

int a = 10 - (5 - 4);

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 .

gnasher729
quelle
practically every language in existence; außer APL: Versuchen Sie (hier) [tryapl.org], (1-2)+3(2), 1-(2+3)(-4) und 1-2+3(auch -4) einzugeben .
Tomsmeding
0

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.

Jinzai
quelle
1
Ich bin mir nicht sicher, ob dies die Frage beantwortet. Die unterstützenden Absätze sind eher verwirrend als hilfreich. Inwiefern ist der Java-Compiler für den C ++ - Compiler relevant?
Adam Zuckerman
OP fragte, ob die Klammern weg waren ... Ich sagte, dass dies der Fall war und erklärte weiter, dass ausführbarer Code nur Zahlen sind, die Opcodes darstellen. Java wurde in einer anderen Antwort angesprochen. Ich denke, es beantwortet die Frage ganz gut ... aber das ist nur meine Meinung. Danke für die Antwort.
Jinzai
3
Klammern erzwingen keine "Reihenfolge der Operation". Sie ändern den Vorrang. Also, in a = f() + (g() + h());den Compiler frei zu nennen f, gund hin dieser Reihenfolge (oder in beliebiger Reihenfolge gefällt ihm).
Alok,
Ich bin mit dieser Aussage einigermaßen nicht einverstanden ... Sie können die Reihenfolge der Operationen absolut mit Klammern erzwingen.
Jinzai
0

Compiler übersetzen, unabhängig von der Sprache, alle Infix-Mathematik in Postfix. Mit anderen Worten, wenn der Compiler etwas sieht wie:

((a+b)+c)

es übersetzt es in das:

 a b + c +

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.

Brian Drozd
quelle
5
Dies ist eine falsche Annahme darüber, wie Compiler Operationen übersetzen. Sie nehmen hier zum Beispiel eine Stapelmaschine an. Was wäre, wenn Sie einen Vektorprozessor hätten? Was wäre, wenn Sie eine Maschine mit einer großen Anzahl von Registern hätten?
Ahmed Masud