Ist es eine schlechte Praxis, Code zu schreiben, der auf Compileroptimierungen beruht?

99

Ich habe etwas C ++ gelernt und muss häufig große Objekte von Funktionen zurückgeben, die innerhalb der Funktion erstellt wurden. Ich weiß, dass es die Referenzübergabe, die Rückgabe eines Zeigers und die Rückgabe einer Referenztyp-Lösung gibt, aber ich habe auch gelesen, dass C ++ - Compiler (und der C ++ - Standard) die Rückgabewertoptimierung ermöglichen, wodurch das Kopieren dieser großen Objekte durch den Speicher vermieden wird Das spart Zeit und Gedächtnis.

Ich bin der Meinung, dass die Syntax viel klarer ist, wenn das Objekt explizit als Wert zurückgegeben wird und der Compiler im Allgemeinen das RVO verwendet, um den Prozess effizienter zu gestalten. Ist es eine schlechte Praxis, sich auf diese Optimierung zu verlassen? Dies macht den Code für den Benutzer klarer und lesbarer, was äußerst wichtig ist. Aber sollte ich vorsichtig sein, wenn ich annehme, dass der Compiler die RVO-Gelegenheit ergreifen wird?

Handelt es sich um eine Mikrooptimierung, oder etwas, das ich beim Entwerfen meines Codes beachten sollte?

Matt
quelle
7
Um auf Ihre Bearbeitung zu antworten, handelt es sich um eine Mikrooptimierung, denn selbst wenn Sie versuchen, das zu messen, was Sie in Nanosekunden verdienen, werden Sie es kaum bemerken. Im Übrigen bin ich in C ++ zu schlecht, um Ihnen eine genaue Antwort zu geben, warum es nicht funktioniert. Eine davon ist wahrscheinlich, dass es Fälle gibt, in denen Sie eine dynamische Zuordnung benötigen und daher new / pointer / references verwenden.
Walfrat
4
@Walfrat, auch wenn die Objekte ziemlich groß sind, in der Größenordnung von Megabyte? Meine Arrays können aufgrund der Art der Probleme, die ich löse, enorm werden.
Matt
6
@Matt würde ich nicht. Genau dafür gibt es Hinweise. Compiler-Optimierungen sollten über das hinausgehen, was Programmierer beim Erstellen eines Programms berücksichtigen sollten, auch wenn sich die beiden Welten häufig überschneiden.
Neil
5
@Matt Sofern Sie nicht etwas extrem Spezifisches tun, das Entwickler mit mehr als 10-jähriger Erfahrung in C / Kernels voraussetzt, sollten Sie dies bei geringen Hardware-Interaktionen nicht benötigen. Wenn Sie der Meinung sind, dass Sie zu etwas sehr Spezifischem gehören, bearbeiten Sie Ihren Beitrag und fügen Sie eine genaue Beschreibung dessen hinzu, was Ihre Anwendung tun soll (Echtzeit? Schwere mathematische Berechnungen? ...)
Walfrat
37
Ja, im speziellen Fall von C ++ (N) RVO ist es absolut richtig, sich auf diese Optimierung zu verlassen. Dies liegt daran, dass der C ++ 17-Standard dies ausdrücklich in Situationen vorschreibt, in denen moderne Compiler dies bereits getan haben.
Caleth

Antworten:

130

Setze das Prinzip des geringsten Erstaunens ein .

Sind Sie und immer nur Sie es, die diesen Code verwenden, und sind Sie sicher, dass das, was Sie in 3 Jahren tun, Sie nicht überraschen wird?

Fahre fort.

Verwenden Sie in allen anderen Fällen die Standardmethode. Andernfalls werden Sie und Ihre Kollegen auf schwer zu findende Fehler stoßen.

Zum Beispiel beschwerte sich mein Kollege über meinen Code, der Fehler verursachte. Es stellte sich heraus, dass er die Kurzschluss-Boolesche Auswertung in seinen Compilereinstellungen deaktiviert hatte. Ich hätte ihn fast geschlagen.

Pieter B
quelle
88
@Neil das ist mein Punkt, jeder verlässt sich auf Kurzschlussauswertung. Und Sie sollten nicht zweimal darüber nachdenken müssen, es sollte eingeschaltet sein. Es ist ein Defacto-Standard. Ja, du kannst es ändern, aber du solltest es nicht.
Pieter B
49
"Ich habe geändert, wie die Sprache funktioniert, und dein schmutziger Code ist kaputt gegangen! Arghh!" Beeindruckend. Ohrfeigen wäre angebracht, schicken Sie Ihren Kollegen zum Zen-Training, da ist viel drin.
109
@PieterB Ich bin mir ziemlich sicher, dass die Sprachspezifikationen für C und C ++ eine Kurzschlussbewertung garantieren. Es ist also nicht nur ein De-facto-Standard, sondern der Standard. Ohne sie verwenden Sie nicht einmal mehr C / C ++, sondern etwas, das verdächtig ist: P
marcelm
47
Die Standardmethode ist hier nur als Referenz die Rückgabe nach Wert.
DeadMG
28
@ dan04 ja es war in delphi. Leute, lasst euch nicht in das Beispiel verwickeln, es geht um den Punkt, den ich gemacht habe. Mach keine überraschenden Dinge, die sonst niemand tut.
Pieter B
81

Geben Sie in diesem speziellen Fall auf jeden Fall nur nach Wert zurück.

  • RVO und NRVO sind bekannte und robuste Optimierungen, die von jedem anständigen Compiler auch im C ++ 03-Modus durchgeführt werden sollten.

  • Die Verschiebungssemantik stellt sicher, dass Objekte aus Funktionen verschoben werden, wenn (N) RVO nicht stattgefunden hat. Das ist nur dann sinnvoll , wenn Ihr Objekt dynamische Daten intern verwendet (wie der std::vectorFall ist), aber das sollte wirklich der Fall sein , wenn es , dass große - überfüllt den Stapel ein Risiko mit großen automatischen Objekten.

  • C ++ 17 erzwingt RVO. Also keine Sorge, es verschwindet nicht auf Ihnen und wird sich erst dann vollständig etablieren, wenn die Compiler auf dem neuesten Stand sind.

Letztendlich sind das Erzwingen einer zusätzlichen dynamischen Zuordnung, um einen Zeiger zurückzugeben, oder das Erzwingen, dass Ihr Ergebnistyp standardmäßig konstruierbar ist, nur damit Sie ihn als Ausgabeparameter übergeben können, hässliche und nicht idiomatische Lösungen für ein Problem, das Sie wahrscheinlich nie finden werden haben.

Schreiben Sie einfach sinnvollen Code und danken Sie den Compiler-Autoren für die korrekte Optimierung des sinnvollen Codes.

QUentin
quelle
9
Erfahren Sie zum Spaß, wie Borland Turbo C ++ 3.0 von 1990 mit RVO umgeht . Spoiler: Es funktioniert im Grunde ganz gut.
NWP
9
Der Schlüssel hierbei ist, dass es sich nicht um eine zufällige compilerspezifische Optimierung oder ein "undokumentiertes Feature" handelt, sondern um etwas, das, obwohl es in mehreren Versionen des C ++ - Standards technisch optional war, von der Branche stark vorangetrieben wurde und für das so ziemlich jeder große Compiler es getan hat eine sehr lange Zeit.
7
Diese Optimierung ist nicht ganz so robust, wie man vielleicht möchte. Ja, es ist in den offensichtlichsten Fällen ziemlich zuverlässig, aber zum Beispiel bei gccs Bugzilla gibt es viele kaum weniger offensichtliche Fälle, in denen es übersehen wird.
Marc Glisse
62

Ich bin der Meinung, dass die Syntax viel klarer ist, wenn das Objekt explizit als Wert zurückgegeben wird und der Compiler im Allgemeinen das RVO verwendet, um den Prozess effizienter zu gestalten. Ist es eine schlechte Praxis, sich auf diese Optimierung zu verlassen? Dies macht den Code für den Benutzer klarer und lesbarer, was äußerst wichtig ist. Aber sollte ich vorsichtig sein, wenn ich annehme, dass der Compiler die RVO-Gelegenheit ergreifen wird?

Dies ist keine wenig bekannte, niedliche Mikrooptimierung, über die Sie in einem kleinen Blog gelesen haben und über die Sie sich dann schlau und überlegen fühlen.

Nach C ++ 11 ist RVO die Standardmethode , um diesen Code zu schreiben. Es ist üblich, zu erwarten, zu unterrichten, in Vorträgen zu erwähnen, in Blogs zu erwähnen, und im Standard zu erwähnen. Wird dies nicht implementiert, wird dies als Compiler-Fehler gemeldet. In C ++ 17 geht die Sprache noch einen Schritt weiter und schreibt in bestimmten Szenarien die Kopierentscheidung vor.

Auf diese Optimierung sollten Sie sich unbedingt verlassen.

Darüber hinaus führt die Rückgabe nach Wert dazu, dass Code wesentlich einfacher gelesen und verwaltet werden kann als Code, der als Referenz zurückgegeben wird. Wertesemantik ist eine mächtige Sache, die selbst zu mehr Optimierungsmöglichkeiten führen könnte.

Barry
quelle
3
Vielen Dank, das macht sehr viel Sinn und steht im Einklang mit dem oben erwähnten "Grundsatz des geringsten Erstaunens". Dies würde den Code sehr klar und verständlich machen und es schwieriger machen, mit Zeiger-Spielereien in Konflikt zu geraten.
Matt
3
@Matt Ein Grund, warum ich diese Antwort positiv bewertet habe, ist, dass darin "Wertsemantik" erwähnt wird. Wenn Sie mehr Erfahrung mit C ++ (und der Programmierung im Allgemeinen) haben, werden Sie gelegentlich Situationen finden, in denen die Wertesemantik für bestimmte Objekte nicht verwendet werden kann, da sie veränderlich sind und ihre Änderungen für anderen Code sichtbar gemacht werden müssen, der dasselbe Objekt verwendet (an Beispiel für "geteilte Veränderbarkeit"). In diesen Situationen müssen die betroffenen Objekte über (intelligente) Zeiger freigegeben werden.
Rwong
16

Die Richtigkeit des von Ihnen geschriebenen Codes sollte niemals von einer Optimierung abhängen. Es sollte das richtige Ergebnis ausgeben, wenn es auf der C ++ - "virtuellen Maschine" ausgeführt wird, die sie in der Spezifikation verwenden.

Worüber Sie jedoch sprechen, ist eher eine Frage der Effizienz. Ihr Code läuft besser, wenn er mit einem RVO-Optimierungscompiler optimiert wird. Aus all den Gründen, auf die in den anderen Antworten hingewiesen wird, ist das in Ordnung.

Wenn Sie jedoch diese Optimierung benötigen (z. B. wenn der Kopierkonstruktor tatsächlich zum Fehlschlagen Ihres Codes führen würde), sind Sie jetzt bei den Launen des Compilers.

Ich denke, das beste Beispiel dafür in meiner eigenen Praxis ist die Tail-Call-Optimierung:

   int sillyAdd(int a, int b)
   {
      if (b == 0)
          return a;
      return sillyAdd(a + 1, b - 1);
   }

Es ist ein albernes Beispiel, aber es zeigt einen Tail-Aufruf, bei dem eine Funktion direkt am Ende einer Funktion rekursiv aufgerufen wird. Die virtuelle C ++ - Maschine wird zeigen, dass dieser Code ordnungsgemäß funktioniert, obwohl ich möglicherweise ein wenig verwirrt darüber bin , warum ich mir überhaupt die Mühe gemacht habe, eine solche Additionsroutine zu schreiben. In praktischen Implementierungen von C ++ verfügen wir jedoch über einen Stapel, und der Speicherplatz ist begrenzt. Bei umständlicher Ausführung müsste diese Funktion beim b + 1Hinzufügen mindestens Stapelrahmen auf den Stapel schieben . Wenn ich rechnen will sillyAdd(5, 7), ist das keine große Sache. Wenn ich rechnen möchte sillyAdd(0, 1000000000), könnte ich wirklich Probleme haben, einen StackOverflow zu verursachen (und nicht die gute Art ).

Wir können jedoch sehen, dass wir, sobald wir diese letzte Rücksprunglinie erreicht haben, wirklich mit allem im aktuellen Stapelrahmen fertig sind. Wir müssen es nicht wirklich behalten. Mit der Tail-Call-Optimierung können Sie den vorhandenen Stack-Frame für die nächste Funktion "wiederverwenden". Auf diese Weise benötigen wir stattdessen nur 1 Stapelrahmen b+1. (Wir müssen immer noch all diese dummen Additionen und Subtraktionen machen, aber sie nehmen nicht mehr Platz in Anspruch.) In der Tat verwandelt die Optimierung den Code in:

   int sillyAdd(int a, int b)
   {
      begin:
      if (b == 0)
          return a;
      // return sillyAdd(a + 1, b - 1);
      a = a + 1;
      b = b - 1;
      goto begin;  
   }

In einigen Sprachen ist die Tail-Call-Optimierung in der Spezifikation explizit erforderlich. C ++ gehört nicht dazu . Ich kann mich nicht darauf verlassen, dass C ++ - Compiler diese Möglichkeit zur Optimierung von Tail Calls erkennen, es sei denn, ich gehe von Fall zu Fall. Bei meiner Version von Visual Studio führt die Release-Version die Tail-Call-Optimierung durch, die Debug-Version jedoch nicht (von Entwurf).

Daher wäre es schlecht für mich, wenn ich darauf angewiesen wäre, rechnen zu können sillyAdd(0, 1000000000).

Cort Ammon
quelle
2
Dies ist ein interessanter Eckfall, aber ich glaube nicht, dass Sie ihn auf die Regel in Ihrem ersten Absatz verallgemeinern können. Angenommen, ich habe ein Programm für ein kleines Gerät, das genau dann geladen wird, wenn ich die Optimierungen des Compilers zur Größenreduzierung verwende - ist das falsch? Es scheint ziemlich umständlich zu sagen, dass meine einzige gültige Wahl darin besteht, es in Assembler umzuschreiben, insbesondere wenn dieses Umschreiben die gleichen Aktionen ausführt wie der Optimierer, um das Problem zu lösen.
Sdenham
5
@sdenham Ich nehme an, es gibt ein wenig Raum in der Auseinandersetzung. Wenn Sie nicht mehr für "C ++", sondern für "WindRiver C ++ Compiler Version 3.4.1" schreiben, kann ich die Logik dort erkennen. Wenn Sie jedoch etwas schreiben, das gemäß der Spezifikation nicht ordnungsgemäß funktioniert, befinden Sie sich in der Regel in einem ganz anderen Szenario. Ich weiß, dass die Boost-Bibliothek solchen Code enthält, aber sie setzen ihn immer in #ifdefBlöcke und bieten eine standardkonforme Problemumgehung an.
Cort Ammon
4
Ist das ein Tippfehler im zweiten Codeblock, in dem es heißt b = b + 1?
Stib
2
Möglicherweise möchten Sie erläutern, was Sie unter einer "virtuellen C ++ - Maschine" verstehen, da dies in keinem Standarddokument verwendet wird. Ich denke, Sie sprechen über das Ausführungsmodell von C ++, sind sich aber nicht ganz sicher - und Ihr Begriff ähnelt täuschend einer "virtuellen Maschine mit Bytecode", die sich auf etwas völlig anderes bezieht.
Toby Speight
1
@supercat Scala hat auch eine explizite Schwanzrekursionssyntax. C ++ ist seine eigene Bestie, aber ich denke, die Schwanzrekursion ist für nicht-funktionale Sprachen unidiomatisch und für funktionale Sprachen obligatorisch, so dass eine kleine Menge von Sprachen übrig bleibt, in denen es sinnvoll ist, eine explizite Schwanzrekursionssyntax zu haben. Die wörtliche Übersetzung der Schwanzrekursion in Schleifen und explizite Mutation ist für viele Sprachen eine bessere Option.
Prosfilaes
8

In der Praxis erwarten C ++ - Programme einige Compileroptimierungen.

Schauen Sie insbesondere in den Standard - Header Ihrer Standard - Container - Implementierungen. Mit GCC können Sie die vorverarbeitete Form ( g++ -C -E) und die GIMPLE-interne Darstellung ( g++ -fdump-tree-gimpleoder Gimple SSA mit -fdump-tree-ssa) der meisten Quelldateien (technische Übersetzungseinheiten) mithilfe von Containern anfordern . Sie werden überrascht sein, wie viel Optimierung (mit g++ -O2) durchgeführt wird. Daher verlassen sich die Implementierer von Containern auf die Optimierungen (und meistens weiß der Implementierer einer C ++ - Standardbibliothek, welche Optimierung stattfinden würde, und schreibt die Containerimplementierung unter Berücksichtigung dieser Aspekte. Manchmal schreibt er auch den Optimierungspass im Compiler an mit den von der Standard-C ++ - Bibliothek benötigten Funktionen umgehen).

In der Praxis sind es die Compiler-Optimierungen, die C ++ und seine Standard-Container effizient genug machen. Darauf können Sie sich verlassen.

Und ebenso für den in Ihrer Frage genannten RVO-Fall.

Der C ++ - Standard wurde mitentwickelt (insbesondere durch Experimentieren mit ausreichend guten Optimierungen, während neue Funktionen vorgeschlagen wurden), um mit den möglichen Optimierungen gut zusammenzuarbeiten.

Betrachten Sie zum Beispiel das folgende Programm:

#include <algorithm>
#include <vector>

extern "C" bool all_positive(const std::vector<int>& v) {
  return std::all_of(v.begin(), v.end(), [](int x){return x >0;});
}

kompiliere es mit g++ -O3 -fverbose-asm -S. Sie werden feststellen, dass die generierte Funktion keine CALLMaschinenanweisungen ausführt . Daher wurden die meisten C ++ - Schritte (Konstruktion eines Lambda-Verschlusses, wiederholte Anwendung, Abrufen von beginund endIteratoren usw.) optimiert. Der Maschinencode enthält nur eine Schleife (die im Quellcode nicht explizit vorkommt). Ohne solche Optimierungen wird C ++ 11 nicht erfolgreich sein.

Nachträge

(hinzugefügt december 31 st 2017)

Siehe CppCon 2017: Matt Godbolt „Was hat mein Compiler in letzter Zeit für mich getan? Öffnen des Compiler-Deckels “ .

Basile Starynkevitch
quelle
4

Wann immer Sie einen Compiler verwenden, müssen Sie wissen, dass er Maschinen- oder Byte-Code für Sie erzeugt. Es gibt keine Garantie dafür, wie der generierte Code aussieht, außer dass der Quellcode gemäß der Spezifikation der Sprache implementiert wird. Beachten Sie, dass diese Garantie unabhängig von der verwendeten Optimierungsstufe gleich ist. Daher gibt es im Allgemeinen keinen Grund, eine Ausgabe als „richtiger“ als die andere zu betrachten.

In Fällen wie RVO, in denen es in der Sprache angegeben ist, erscheint es außerdem sinnlos, sich Mühe zu geben, es nicht zu verwenden, insbesondere wenn dadurch der Quellcode einfacher wird.

Es wird viel Mühe darauf verwendet, dass Compiler effiziente Ergebnisse erzielen, und es ist klar, dass diese Funktionen genutzt werden sollen.

Es kann Gründe dafür geben, nicht optimierten Code zu verwenden (zum Beispiel für das Debuggen), aber der in dieser Frage erwähnte Fall scheint kein solcher zu sein (und wenn Ihr Code nur im optimierten Zustand fehlschlägt und keine Konsequenz einer Besonderheit des ist) Gerät, auf dem Sie es ausführen, dann ist irgendwo ein Fehler aufgetreten, und es ist unwahrscheinlich, dass es sich im Compiler befindet.)

Sdenham
quelle
3

Ich denke, andere haben den spezifischen Aspekt von C ++ und RVO gut abgedeckt. Hier ist eine allgemeinere Antwort:

Wenn es um Korrektheit geht, sollten Sie sich nicht auf Compileroptimierungen oder compilerspezifisches Verhalten im Allgemeinen verlassen. Zum Glück scheinen Sie das nicht zu tun.

Wenn es um Leistung geht, Sie müssen auf Compiler-spezifische Verhalten im Allgemeinen verlassen, und Compiler - Optimierungen im Besonderen. Ein standardkonformer Compiler kann Ihren Code nach Belieben kompilieren, solange sich der kompilierte Code gemäß der Sprachspezifikation verhält. Und mir ist keine Spezifikation für eine Standardsprache bekannt, die angibt, wie schnell jeder Vorgang sein muss.

svick
quelle
1

Compiler-Optimierungen sollten sich nur auf die Leistung auswirken, nicht auf die Ergebnisse. Es ist nicht nur vernünftig, sich auf Compiler-Optimierungen zu verlassen, um nichtfunktionalen Anforderungen gerecht zu werden. Dies ist häufig der Grund, warum ein Compiler über einen anderen gestellt wird.

Flags, die bestimmen, wie bestimmte Vorgänge ausgeführt werden (z. B. Index- oder Überlaufbedingungen), werden häufig mit Compiler-Optimierungen in Verbindung gebracht, sollten dies jedoch nicht. Sie wirken sich explizit auf die Ergebnisse von Berechnungen aus.

Wenn eine Compiler-Optimierung zu unterschiedlichen Ergebnissen führt, ist dies ein Fehler - ein Fehler im Compiler. Auf einen Fehler im Compiler zu vertrauen, ist auf lange Sicht ein Fehler - was passiert, wenn er behoben wird?

Die Verwendung von Compiler-Flags, die die Funktionsweise von Berechnungen ändern, sollte gut dokumentiert sein, aber bei Bedarf verwendet werden.

jmoreno
quelle
Leider kann in vielen Compiler-Dokumentationen nicht genau angegeben werden, was in den verschiedenen Modi garantiert ist oder nicht. Außerdem scheinen "moderne" Compiler-Autoren die Kombinationen von Garantien, die Programmierer tun und nicht brauchen, nicht zu kennen. Wenn ein Programm gut funktionieren würde, wenn es x*y>zim Falle eines Überlaufs willkürlich 0 oder 1 ergibt, vorausgesetzt, es hat keine anderen Nebenwirkungen , und ein Programmierer muss entweder um jeden Preis Überläufe verhindern oder den Compiler zwingen, den Ausdruck auf eine bestimmte Weise auszuwerten unnötige
Beeinträchtigung
... der Compiler kann sich in seiner Freizeit so verhalten, als würde er x*yseine Operanden auf einen willkürlich längeren Typ heraufstufen (wodurch Formen des Hebens und der Festigkeitsreduzierung möglich werden, die das Verhalten einiger Überlauffälle ändern würden). Viele Compiler verlangen jedoch, dass Programmierer entweder um jeden Preis einen Überlauf verhindern oder Compiler zwingen, im Falle eines Überlaufs alle Zwischenwerte abzuschneiden.
Supercat
1

Nein.

Das mache ich die ganze Zeit. Wenn ich auf einen beliebigen 16-Bit-Block im Speicher zugreifen muss, tue ich dies

void *ptr = get_pointer();
uint16_t u16;
memcpy(&u16, ptr, sizeof(u16)); // ntohs omitted for simplicity

... und sich darauf verlassen, dass der Compiler alles tut, um diesen Code zu optimieren. Der Code funktioniert auf ARM, i386, AMD64 und praktisch auf jeder einzelnen Architektur. Theoretisch könnte ein nicht optimierender Compiler tatsächlich aufrufen memcpy, was zu einer völlig schlechten Leistung führt, aber das ist für mich kein Problem, da ich Compileroptimierungen verwende.

Betrachten Sie die Alternative:

void *ptr = get_pointer();
uint16_t *u16ptr = ptr;
uint16_t u16;
u16 = *u16ptr;  // ntohs omitted for simplicity

Dieser alternative Code funktioniert nicht auf Computern, die eine ordnungsgemäße Ausrichtung erfordern, wenn get_pointer()ein nicht ausgerichteter Zeiger zurückgegeben wird. Alternativ können auch Aliasing-Probleme auftreten.

Der Unterschied zwischen -O2 und -O0 bei der Verwendung des memcpyTricks ist groß: 3,2 Gbit / s IP-Prüfsummenleistung im Vergleich zu 67 Gbit / s IP-Prüfsummenleistung. Über eine Größenordnung Unterschied!

Manchmal müssen Sie möglicherweise dem Compiler helfen. Anstatt sich beispielsweise darauf zu verlassen, dass der Compiler Schleifen auflöst, können Sie dies selbst tun. Entweder durch Implementierung des berühmten Duff-Geräts oder auf eine sauberere Art und Weise.

Der Nachteil, sich auf die Compiler-Optimierungen zu verlassen, besteht darin, dass Sie, wenn Sie gdb ausführen, um Ihren Code zu debuggen, möglicherweise feststellen, dass viel weg optimiert wurde. Daher müssen Sie möglicherweise mit -O0 neu kompilieren, was bedeutet, dass die Leistung beim Debuggen völlig sinkt. Ich halte dies für einen Nachteil, wenn man die Vorteile der Optimierung von Compilern berücksichtigt.

Was auch immer Sie tun, stellen Sie bitte sicher, dass Ihr Weg tatsächlich nicht undefiniertes Verhalten ist. Der Zugriff auf einen zufälligen Speicherblock als 16-Bit-Ganzzahl ist aufgrund von Aliasing- und Ausrichtungsproblemen undefiniert.

juhist
quelle
0

Alle Versuche, effizienten Code in einer anderen Form als Assembly zu schreiben, beruhen sehr, sehr stark auf Compiler-Optimierungen, angefangen bei der einfachsten wie effizienten Registerzuweisung, um überflüssige Stapelüberläufe zu vermeiden, und einer zumindest einigermaßen guten, wenn nicht ausgezeichneten Anweisungsauswahl. Andernfalls wären wir in die 80er Jahre zurückgekehrt, wo wir registerüberall Hinweise setzen und die minimale Anzahl von Variablen in einer Funktion verwenden müssten , um archaischen C-Compilern zu helfen, oder sogar früher, als dies gotoeine nützliche Verzweigungsoptimierung war.

Wenn wir uns nicht darauf verlassen könnten, dass unser Optimierer unseren Code optimiert, würden wir alle weiterhin leistungskritische Ausführungspfade in der Assembly codieren.

Es ist wirklich eine Frage der Zuverlässigkeit, mit der Sie die Optimierung durchführen können, die am besten aussortiert werden kann, indem Sie die Fähigkeiten Ihrer Compiler profilieren und untersuchen und möglicherweise sogar disassemblieren, wenn es einen Hotspot gibt, an dem Sie nicht herausfinden können, wo der Compiler sich befindet haben es versäumt, eine offensichtliche Optimierung vorzunehmen.

RVO gibt es schon seit Ewigkeiten, und Compiler wenden es seit Ewigkeiten zuverlässig an, zumindest sehr komplexe Fälle auszuschließen. Es lohnt sich definitiv nicht, ein Problem zu umgehen, das es nicht gibt.

Err auf der Seite, sich auf den Optimierer zu verlassen und ihn nicht zu fürchten

Im Gegenteil, ich würde sagen, dass man sich eher auf Compiler-Optimierungen als auf zu wenig verlässt. Dieser Vorschlag kommt von einem Mann, der in sehr leistungskritischen Bereichen arbeitet, in denen Effizienz, Wartbarkeit und wahrgenommene Qualität bei Kunden sind alles eine riesige Unschärfe. Ich würde es vorziehen, wenn Sie sich zu selbstbewusst auf Ihren Optimierer verlassen und obskure Randfälle finden, in denen Sie sich zu sehr verlassen, als sich auf zu wenig zu verlassen und für den Rest Ihres Lebens nur aus abergläubischen Ängsten zu codieren. Zumindest müssen Sie dann nach einem Profiler greifen und ordnungsgemäß nachforschen, wenn die Dinge nicht so schnell ablaufen, wie sie sollten, und dabei wertvolles Wissen und nicht Aberglauben erwerben.

Es ist gut, sich auf den Optimierer zu stützen. Mach weiter. Werden Sie nicht zu dem Typ, der explizit verlangt, jede in einer Schleife aufgerufene Funktion inline zu setzen, bevor Sie aus Angst vor den Mängeln des Optimierers ein Profil erstellen.

Profiling

Profiling ist wirklich der Kreisverkehr, aber die ultimative Antwort auf Ihre Frage. Das Problem, mit dem Anfänger, die gerne effizienten Code schreiben, häufig zu kämpfen haben, ist, nicht zu optimieren, sondern nicht zu optimieren, da sie alle möglichen fehlgeleiteten Vermutungen über Ineffizienzen entwickeln, die zwar menschlich intuitiv sind, aber rechnerisch falsch. Wenn Sie Erfahrungen mit einem Profiler sammeln, können Sie nicht nur die Optimierungsfunktionen Ihrer Compiler richtig einschätzen, auf die Sie sich sicher verlassen können, sondern auch die Funktionen (sowie die Einschränkungen) Ihrer Hardware. Es ist wohl noch wertvoller, ein Profil zu erstellen, wenn man lernt, was sich nicht zu optimieren lohnt, als zu lernen, was es war.


quelle
-1

Software kann in C ++ auf sehr unterschiedlichen Plattformen und für viele verschiedene Zwecke geschrieben werden.

Dies hängt vollständig vom Zweck der Software ab. Sollte es einfach zu warten, zu erweitern, zu patchen, umzugestalten usw. sein. oder sind andere Dinge wichtiger, wie die Leistung, die Kosten oder die Kompatibilität mit einer bestimmten Hardware oder die Zeit, die für die Entwicklung benötigt wird.

mathreadler
quelle
-2

Ich denke, die langweilige Antwort darauf lautet: "Es kommt darauf an".

Ist es eine schlechte Praxis, Code zu schreiben, der sich auf eine Compiler-Optimierung stützt, die wahrscheinlich deaktiviert ist und bei der die Sicherheitsanfälligkeit nicht dokumentiert ist und bei der der fragliche Code nicht einheitlich getestet wird, sodass Sie ihn wissen, wenn er kaputt geht ? Wahrscheinlich.

Ist es eine schlechte Praxis, Code zu schreiben, der auf einer Compileroptimierung beruht, die wahrscheinlich nicht deaktiviert wird , die dokumentiert und Unit-getestet ist ? Vielleicht nicht.

Dave Cousineau
quelle
-6

Wenn Sie uns nicht mehr mitteilen, ist dies eine schlechte Praxis, jedoch nicht aus dem von Ihnen vorgeschlagenen Grund.

Möglicherweise wird bei der Rückgabe des Werts eines Objekts in C ++ im Gegensatz zu anderen Sprachen, die Sie zuvor verwendet haben, eine Kopie des Objekts erstellt. Wenn Sie dann das Objekt ändern, ändern Sie ein anderes Objekt . Das heißt, wenn ich habe Obj a; a.x=1;und Obj b = a;dann tue ich b.x += 2; b.f();, dann ist es a.ximmer noch 1, nicht 3.

Nein, die Verwendung eines Objekts als Wert anstelle eines Verweises oder Zeigers bietet nicht die gleiche Funktionalität, und es kann zu Fehlern in Ihrer Software kommen.

Vielleicht wissen Sie das und es wirkt sich nicht negativ auf Ihren speziellen Anwendungsfall aus. Anhand des Wortlauts in Ihrer Frage scheint Ihnen der Unterschied jedoch möglicherweise nicht bewusst zu sein. Formulierung wie "Objekt in der Funktion erstellen"

"Objekt in der Funktion erstellen" klingt wie new Obj;"Objekt nach Wert zurückgeben"Obj a; return a;

Obj a;und Obj* a = new Obj;sind sehr, sehr verschiedene Dinge; Ersteres kann zu Speicherbeschädigung führen, wenn es nicht richtig verwendet und verstanden wird, und Letzteres kann zu Speicherlecks führen, wenn es nicht richtig verwendet und verstanden wird.

Aaron
quelle
8
Die Rückgabewertoptimierung (Return Value Optimization, RVO) ist eine genau definierte Semantik, bei der der Compiler ein zurückgegebenes Objekt eine Ebene höher im Stapelrahmen erstellt, um insbesondere unnötige Objektkopien zu vermeiden. Dies ist ein genau definiertes Verhalten, das lange vor seiner Einführung in C ++ 17 unterstützt wurde. Noch vor 10-15 Jahren haben alle großen Compiler diese Funktion unterstützt und dies konsequent getan.
@Snowman Ich spreche nicht von der physischen Speicherverwaltung auf niedriger Ebene, und ich habe weder über Speicherüberlastung noch über Geschwindigkeit gesprochen. Wie ich in meiner Antwort ausdrücklich gezeigt habe, spreche ich von den logischen Daten. Wenn Sie den Wert eines Objekts angeben , wird logischerweise eine Kopie davon erstellt, unabhängig davon, wie der Compiler implementiert ist oder welche Assembly im Hintergrund verwendet wird. Das hinter den Kulissen Geringfügige ist eine Sache, und die logische Struktur und das Verhalten der Sprache ist eine andere; sie sind verwandt, aber sie sind nicht dasselbe - beide sollten verstanden werden.
Aaron
6
Ihre Antwort lautet: "Wenn Sie den Wert eines Objekts in C ++ zurückgeben, erhalten Sie eine Kopie des Objekts", was im Kontext von RVO völlig falsch ist. Das Objekt wird direkt am aufrufenden Speicherort erstellt, und es wird nie eine Kopie erstellt. Sie können dies testen, indem Sie den Kopierkonstruktor löschen und das Objekt zurückgeben , das in der returnAnweisung erstellt wurde, die für RVO erforderlich ist. Außerdem sprechen Sie dann über Schlüsselwörter newund Zeiger, worum es bei RVO nicht geht. Ich glaube, Sie verstehen die Frage entweder nicht oder RVO oder möglicherweise beides.
-7

Pieter B ist absolut richtig darin, das geringste Erstaunen zu empfehlen.

Um Ihre spezifische Frage zu beantworten, bedeutet dies (höchstwahrscheinlich) in C ++, dass Sie a std::unique_ptran das konstruierte Objekt zurückgeben sollten.

Der Grund ist, dass dies für einen C ++ - Entwickler klarer ist , was los ist.

Obwohl Ihr Ansatz höchstwahrscheinlich funktionieren würde, signalisieren Sie effektiv, dass das Objekt ein kleiner Werttyp ist, obwohl dies nicht der Fall ist. Darüber hinaus werfen Sie alle Möglichkeiten der Schnittstellenabstraktion weg. Dies mag für Ihre derzeitigen Zwecke in Ordnung sein, ist jedoch häufig sehr nützlich, wenn Sie sich mit Matrizen befassen.

Ich schätze, dass, wenn Sie aus anderen Sprachen kommen, alle Siegel anfangs verwirrend sein können. Achten Sie jedoch darauf, nicht anzunehmen, dass Sie Ihren Code klarer machen, wenn Sie ihn nicht verwenden. In der Praxis dürfte das Gegenteil der Fall sein.

Alex
quelle
Wenn du in Rom bist, mach wie es die Römer tun.
14
Dies ist keine gute Antwort für Typen, die selbst keine dynamischen Zuweisungen durchführen. Dass der OP das natürliche Gefühl hat, in seinem Anwendungsfall als Wert zurückzukehren, zeigt an, dass seine Objekte auf der Anruferseite eine automatische Speicherdauer haben. Für einfache, nicht zu große Objekte ist sogar eine naive Implementierung von Copy-Return-Werten um Größenordnungen schneller als eine dynamische Zuordnung. (Wenn die Funktion andererseits einen Container zurückgibt, kann die Rückgabe eines unique_pointer sogar vorteilhaft sein, verglichen mit einer naiven Compiler-Rückgabe nach Wert.)
Peter A. Schneider,
9
@Matt Falls Sie nicht bemerkt haben, dass dies nicht die beste Vorgehensweise ist. Das unnötige Ausführen von Speicherzuordnungen und das Erzwingen der Zeigersemantik für Benutzer ist schlecht.
NWP
5
Wenn Sie intelligente Zeiger verwenden, sollten Sie zunächst ein zurückgeben std::make_unique, nicht ein std::unique_ptrdirektes. Zweitens handelt es sich bei RVO nicht um eine esoterische, herstellerspezifische Optimierung, sondern um eine Integration in den Standard. Auch damals, als es nicht war, war es weithin unterstützt und erwartetes Verhalten. Es gibt keinen Punkt, an dem ein zurückgegeben wird, std::unique_ptrwenn ein Zeiger überhaupt nicht benötigt wird.
4
@Schneemann: Es gibt kein "als es nicht war". Obwohl es erst seit kurzem obligatorisch ist , hat jeder C ++ - Standard jemals [N] RVO erkannt und Anpassungen vorgenommen, um dies zu ermöglichen (z. B. dem Compiler wurde immer die ausdrückliche Erlaubnis erteilt, die Verwendung des Kopierkonstruktors für den Rückgabewert zu unterlassen, auch wenn dies der Fall ist hat sichtbare Nebenwirkungen).
Jerry Coffin