Hallo,
ich habe eine Methode in welcher ein großes Objekt mit Daten erzeugt
wird
Dieses muss ich danach weiter an andere Funktionen / Klassen
weitergeben.
Um zu vermeiden dass dabei immer alles kopiert wird würde ich gerne den
Zeiger auf das Objekt übergeben.
DataObject* myClass::getData()
{
DataObject data;
... füllen mit Daten usw.
return &data;
}
aber dann ist der Speicher nach verlassen der getData() Methode ja
wieder freigegeben oder?
wenn ich alternativ den speicher mit new alloziere muss ich die ganze
zeit auf den Zeiger aufpassen.
Wie kann man das denn sinnvoll lösen?
c-noob schrieb:> Wie kann man das denn sinnvoll lösen?
In dem Du erst einmal darauf vertraust, dass Dein Compiler eine Return
Value Optimization implementiert hat:
1
DataObject myClass::getData()
2
{
3
DataObject data;
4
... füllen mit Daten usw.
5
return data;
6
}
Und selbst wenn nicht, solltest Du erst einmal dafür sorgen, dass Dein
zu lösendes Problem korrekt gelöst ist. Wenn Du dann auf Performace
Probleme stößt, solltest Du vor allem erst einmal Messen, wo es sich am
meisten lohnt, den Aufwand für Optimierung zu treiben.
Rules of Optimization (http://wiki.c2.com/?RulesOfOptimization):
- Don't do it!
- Do it later!
- Profile before optimize!
mfg Torsten
Torsten R. schrieb:> In dem Du erst einmal darauf vertraust, dass Dein Compiler eine Return> Value Optimization implementiert hat:
sehr mutig.
> Und selbst wenn nicht, solltest Du erst einmal dafür sorgen, dass Dein> zu lösendes Problem korrekt gelöst ist. Wenn Du dann auf Performace> Probleme stößt, solltest Du vor allem erst einmal Messen, wo es sich am> meisten lohnt, den Aufwand für Optimierung zu treiben.
auch nicht pauschal, wenn man weis das hier sinnlos großen Datenmengen
erzeugt und verschoben werden, dann kann man gleich bei Design
berücksichtigen. Nur weil es eventuell auch geht, muss man keine
Ressourcen sinnlos verschwenden.
Verhindern das Objekte sinnlos kopiert werden ist einfach eine saubere
Programmierung und hat noch wenig mit Optimierung zu tun.
Danke schon mal für die schnellen Antworten,
smarte Pointer habe ich keine, weil ich kein boost oder ähnliches
verwenden will.
Was ich mir gerade überlegt hatte ist folgendes: ich kann ja die Daten
in dem DataObjet selbst auf dem Heap allozieren und in dem Objekt
verwalten, und dann das Objekt by Value übergeben, das ist ja klein weil
nur Zeiger drin?
Spricht da was dagegen?
c-noob schrieb:> smarte Pointer habe ich keine, weil ich kein boost oder ähnliches> verwenden will.
das ist Standard C++ - dafür braucht man keine boost. Die sind genau für
dein Problem vorhanden.
> Spricht da was dagegen?
das du dann sehr aufpassen musst, wie das Objekt kopiert wird. Sonst
wird der Speicher 2 mal freigeben.
Peter II schrieb:> Torsten R. schrieb:>> In dem Du erst einmal darauf vertraust, dass Dein Compiler eine Return>> Value Optimization implementiert hat:>> sehr mutig.
Nein, wenn man keinen uralten Compiler verwendet, oder die Optimierung
nicht einschaltet, dann gibt es überhaupt keinen Grund, warum der
Compiler diese Optimierung nicht machen sollte.
> Verhindern das Objekte sinnlos kopiert werden ist einfach eine saubere> Programmierung und hat noch wenig mit Optimierung zu tun.
Warum sollte der OP sich hier mit Mirco-Optimierungen bereits das Design
versauen, wenn jeder vernünftige Compiler die nötige Optimierung
implementiert?
"Premature Pessimism" führt nur zu Lösungen, die später keiner mehr
nachvollziehen kann. Wenn Du einen modernen C++ compiler hast, der diese
Optimierung nicht macht, dann ist der Compiler kaputt.
Und selbst wenn der Compiler diese Optimierung nicht macht, kannst Du
dem zu bewegenden Objekt in C++ sehr einfach Move-Semantik, nachträglich
verpassen. Das kann man sich dann aber auch sparen, bis es wirklich ein
zu lösendes Problem gibt.
"Premature Optimization is the Root of all Evil" und hat nur wenig mit
"Sauber" zu tun.
Ein klares Design sorgt sehr häufig dafür, dass die Software sehr
schnell seine Aufgabe erfüllt. Dass läßt dann Raum für Optimierungen an
den Stellen, an denen es sich lohnt. (Und nein, dass bedeutet natürlich
nicht, dass man total planlos agieren sollte)
c-noob schrieb:> smarte Pointer habe ich keine, weil ich kein boost oder ähnliches> verwenden will.
std::shared_ptr / std::unique_ptr sind schon seit 2011 Teil von C++.
Gibt es einen Grund, warum Du Boost nicht verwenden willst?
> Was ich mir gerade überlegt hatte ist folgendes: ich kann ja die Daten> in dem DataObjet selbst auf dem Heap allozieren und in dem Objekt> verwalten, und dann das Objekt by Value übergeben, das ist ja klein weil> nur Zeiger drin?> Spricht da was dagegen?
Weil Du dann entweder ein eigene Owner-Ship-Schema implementieren
müsstest, oder im Fall, dass ein Objekt kopiert wird, eben auch ein deep
copy machen müsstest.
KISS!
Torsten R. schrieb:> Warum sollte der OP sich hier mit Mirco-Optimierungen bereits das Design> versauen, wenn jeder vernünftige Compiler die nötige Optimierung> implementiert?
also bist du auch jemand der eine Datei öffnen, ein Byte reinschreibt
und dann wieder schließt statt die Datei geöffnet zu lassen? Nur weil
der Code dann überschaubarer ist?
Zu verhindern das Objekte kopiert werden ist für mich keine großartige
Optimierung sondern einfach ein sinnvoller Umgang mit Rechenzeit.
Darüber denke ich gar nicht weiter nach sondern mache es einfach.
Wenn das Objekt sehr groß ist, dann braucht man sogar den doppelten
Speicher.
> Nein, wenn man keinen uralten Compiler verwendet, oder die Optimierung> nicht einschaltet, dann gibt es überhaupt keinen Grund, warum der> Compiler diese Optimierung nicht machen sollte.
also ob jeder immer den neusten Compiler einsetzen kann und dann muss
man auch noch prüfen ob er es macht. Im Debugmodus macht er es
vermutlich nicht, was zum nächsten Problem führen kann.
Torsten R. schrieb:> Warum sollte der OP sich hier mit Mirco-Optimierungen bereits das Design> versauen, wenn jeder vernünftige Compiler die nötige Optimierung> implementiert?
Nachtrag:
schreibt du
Peter II schrieb:> Torsten R. schrieb:>> In dem Du erst einmal darauf vertraust, dass Dein Compiler eine Return>> Value Optimization implementiert hat:>> sehr mutig.
???
Copy Elision an dieser Stelle wird jeder Compiler immer machen, seit
C++17 ist sie in diesem Fall sogar vom Standard vorgeschrieben.
Sven B. schrieb:> ???> Copy Elision an dieser Stelle wird jeder Compiler immer machen,> Da nicht alle Kompiler copy elison in jeder erlaubten Situation benutzen> (z.B. ohne Optimierung), sind Programme, die auf den Nebenwirkungen von >
Copy-bzw. Move- (seit C++11)Konstruktoren und Destruktoren angewiesen >sind, nicht
ohne weiteres portierbar.
wer sich gerne selber Steine in den weg legt, kann es gerne tun.
>> ist auch nur eine Optimierung.
Peter, genau wegen solcher "Beispiele" habe ich extra: "Und nein, dass
bedeutet natürlich nicht, dass man total planlos agieren sollte"
geschrieben. Passing by const ref ist der Default für Objekt-Typen in
C++-
Folgen wir doch mal Deinem Vorschlag (oh, ich sehe gerade, dass Du
selbst keinen Vorschlag gemacht hast; also unterstelle ich Dir mal, dass
Du std::unique_ptr<> vorschlagen würdest):
1
const std::unique_ptr< const DataObject > result = obj.getData();
Führt ja offensichtlich schon mal zu einer Allokation mehr, als nötig
(bei shared_ptr<> wären es sogar 2). Und Du musst Dich unnötigerweise
für einen Pointer-Typen entscheiden. Klingt jetzt nach relativ viel
Kompromissen, nur um die Fälle a) "Compiler ist kaput" und b) "Daten
Kopieren könnte teuer werde" unnötig früh im Design zu berücksichtigen.
Wenn das Kopieren des Objekts wirklich ein Problem wird, dann bekommt
DataObject nachträglich, nach dem sich herausgestellt hat, dass diese
Optimierung sinnvoll sein könnte einen Move c'tor.
Aber: Wenn der OP beim Design von DataObject nicht zuviel
Micro-Optimierung gemacht hat, wird aber bereits der vom compiler
erzeugte move c'tor schon das Richtige machen und damit selbst für den
Fall, dass der Compiler RVO nicht implementiert schneller sein, als
Deine Lösung.
mfg Torsten
Torsten R. schrieb:> oh, ich sehe gerade, dass Du> selbst keinen Vorschlag gemacht hast
dann sollte du noch mal lesen
> Klingt jetzt nach relativ viel> Kompromissen, nur um die Fälle a) "Compiler ist kaput" und b) "Daten> Kopieren könnte teuer werde" unnötig früh im Design zu berücksichtigen.
ich kenne den Compiler nicht und weis von uns, das wir einen alten
verwenden der es nicht kann.
> Führt ja offensichtlich schon mal zu einer Allokation mehr, als nötig> (bei shared_ptr<> wären es sogar 2).
spielt keine rolle wenn man im vergleich ein sehr großen Objekt kopieren
muss.
> Fall, dass der Compiler RVO nicht implementiert schneller sein, als> Deine Lösung.
ob die Größe von dem Objekt zu kennen, sehr mutige aussage.
Ok, das Problem mit dem Kopieren mit dem Pointer in der Klasse stimmt
natürlich. Da muss man dann genauso aufpassen.
ich denke ich werde dann den std::shared_ptr verwenden.
Danke für den Hilfe.
Peter II schrieb:> ich kenne den Compiler nicht und weis von uns, das wir einen alten> verwenden der es nicht kann.
Und deswegen schlägst Du jetzt vor, premature optimization zu betreiben?
>> Führt ja offensichtlich schon mal zu einer Allokation mehr, als nötig>> (bei shared_ptr<> wären es sogar 2).> spielt keine rolle wenn man im vergleich ein sehr großen Objekt kopieren> muss.
Kommt auf die Objekt-Größe an. Müsste man halt mal messen...
>> Fall, dass der Compiler RVO nicht implementiert schneller sein, als>> Deine Lösung.> ob die Größe von dem Objekt zu kennen, sehr mutige aussage.
Ne, überhaupt nicht. Wenn ich nach dem Messen zum Ergebnis komme, dass
a) mein Compiler kaput ist und b) das Kopieren deutlich teuerer als eine
zusätzliche Allokation ist und c) ich nicht auf einen vernünftigen
Compiler ausweichen kann, dann kann ich durch eine sehr lokale
Optimierung in DataObject das "Problem" lösen:
1
class DataObject
2
{
3
public:
4
std::size size() const
5
{
6
return pimpl_->size();
7
}
8
9
private:
10
struct impl;
11
12
std::unique_ptr< impl, void(*)( impl* ) > pimpl_;
13
};
Im Fall, dass die Optimierung nötig ist (a und b und c gegeben sind),
kaufe ich mir das durch eine zusätzliche Allokierung und durch eine
Indirektion beim Zugriff. Ein Nachteil, den Deine Lösung schon von
Anfang an hat.
Wenn DataObject Instanzen groß sind, weil sie z.B. einen vector mit sehr
vielen Objekten enthält, dann reicht mir aber schon, was mir der
Compiler implementiert:
1
class DataObject
2
{
3
public:
4
std::size size() const
5
{
6
return data_.size();
7
}
8
9
private:
10
struct record {...};
11
std::vector< record > data_;
12
};
Hier wird der move c'tor beim Kopieren einfach drei Zeiger in
std::vector austauschen.
Also: Warum etwas optimieren, was nur unter sehr engen Randbedingungen
ein Problem sein kann?
Peter II schrieb:> Sven B. schrieb:>> ???>> Copy Elision an dieser Stelle wird jeder Compiler immer machen,>> Da nicht alle Kompiler copy elison in jeder erlaubten Situation benutzen> (z.B. ohne Optimierung), sind Programme, die auf den Nebenwirkungen von> Copy-bzw. Move- (seit C++11)Konstruktoren und Destruktoren angewiesen> sind, nicht> ohne weiteres portierbar.>> wer sich gerne selber Steine in den weg legt, kann es gerne tun.
Dann ist es, zumindest nach dem 17er-Standard kein C++-Compiler und auch
kein C++-Programm. Die Kopie darf gar nicht durchgeführt werden. Steht
im Standard.
Sich nicht auf RVO zu verlassen ist völliger Unsinn. Die zusätzliche
Heap Allocation kostet dich einen Haufen Zeit. Durch die "Optimierung"
hast du genau das Gegenteil bewirkt ...
Sven B. schrieb:> Dann ist es, zumindest nach dem 17er-Standard kein C++-Compiler und auch> kein C++-Programm. Die Kopie darf gar nicht durchgeführt werden. Steht> im Standard.
du behauptet also, das alle alten C++ Compiler ihren Status verlieren?
Es gibt genug Gründe auch alten Compiler einzusetzen, weil die neuen
eventuell nicht Zertifiziert für jeden einsatzzweck sind.
Und nur weil ein Compiler den 17er Standard nicht unterstützt es doch
immer noch ein C++ Compiler.
Sven B. schrieb:> Sich nicht auf RVO zu verlassen ist völliger Unsinn. Die zusätzliche> Heap Allocation kostet dich einen Haufen Zeit. Durch die "Optimierung"> hast du genau das Gegenteil bewirkt ...
nicht jeder Variable landet auf dem Heap.
Peter II schrieb:> Sven B. schrieb:>> Sich nicht auf RVO zu verlassen ist völliger Unsinn. Die zusätzliche>> Heap Allocation kostet dich einen Haufen Zeit. Durch die "Optimierung">> hast du genau das Gegenteil bewirkt ...>> nicht jeder Variable landet auf dem Heap.
Aber jede die du mit make_shared, make_unique, oder new anlegst. ...
> du behauptet also, das alle alten C++ Compiler ihren Status verlieren?
Das ist doch alles gar nicht der Punkt. Du versuchst Menschen zu
erklären man könne sich in diesem Fall nicht auf das Vorhandensein von
RVO verlassen. Das ist unsinnig. Ich habe lediglich untermalt wie
unsinnig das ist, indem ich angemerkt habe, dass es ab C++17 sogar
Pflicht für Compiler ist, diese Optimierung vorzunehmen.
Sven B. schrieb:> Das ist doch alles gar nicht der Punkt. Du versuchst Menschen zu> erklären man könne sich in diesem Fall nicht auf das Vorhandensein von> RVO verlassen. Das ist unsinnig.
wenn man nicht den neusten Compiler einsetzt ist das kein Unsinn.
ein Visual-Studio 2015 Compiler macht es nicht.
Peter II schrieb:> ein Visual-Studio 2015 Compiler macht es nicht.
Nachtrag:
zumindest nicht in der Debug Version.
Man handelt sich also durchaus Probleme ein, die man vermeiden kann.
Peter II schrieb:> Peter II schrieb:>> ein Visual-Studio 2015 Compiler macht es nicht.>> Nachtrag:> zumindest nicht in der Debug Version.
Genau. In der du nie kompilierst, wenn du irgendwie über Performance
redest.
Wenn du nicht gerade irgendwie das VS von 1997 nimmst macht der das
wahrscheinlich seit immer ...
Peter II schrieb:> Peter II schrieb:>> ein Visual-Studio 2015 Compiler macht es nicht.>> Nachtrag:> zumindest nicht in der Debug Version.>> Man handelt sich also durchaus Probleme ein, die man vermeiden kann.
Was kommt jetzt als nächstes? Der OP sollte std::vector nicht nutzen,
weil im Debug build jede Menge Prüfungen implementiert sind, die die SW
langsam machen?
Man kann übrigens auch bei einem Microsoft-Compiler für den Debug Build
die Optimierung einschalten / auswählen.
Torsten R. schrieb:> Was kommt jetzt als nächstes?
was soll noch kommen? Ich habe meine Meinung gesagt und einen
SmartPointer vorgeschlagen, das du andere Meinung bist ist doch ok.
Es gibt verschieden Wege ein Problem zu lösen. Wir wissen weder welchen
Compiler genutzt wird, noch wie groß das Objekt wirklich ist.
Es macht überhaupt keinen sinn darüber weiter zu streiten.
c-noob schrieb:> Was ich mir gerade überlegt hatte ist folgendes: ich kann ja die Daten> in dem DataObjet selbst auf dem Heap allozieren und in dem Objekt> verwalten, und dann das Objekt by Value übergeben, das ist ja klein weil> nur Zeiger drin?> Spricht da was dagegen?
Nicht das es generell sinnvoll ist, aber vielleicht kann DataObject auch
im Aufrufer erzeugt werden und getData() wird ein fillData(*obj) oder
initData(*obj).
Sven B. schrieb:> Wenn du nicht gerade irgendwie das VS von 1997 nimmst macht der das> wahrscheinlich seit immer ...
Schon MSVC 1.5.2 (1993) konnte RVO, genauso wie g++ 2.45 (1993) und
Borland Turbo C++ 3.0 (1992). Die Möglichkeiten sind eingeschränkt
verglichen mit dem was heute möglich ist (z.B. kein NRVO), aber sie
wurden auch ohne Optimierung (-O0) durchgeführt.
Achim S. schrieb:> c-noob schrieb:>> Was ich mir gerade überlegt hatte ist folgendes: ich kann ja die Daten>> in dem DataObjet selbst auf dem Heap allozieren und in dem Objekt>> verwalten, und dann das Objekt by Value übergeben, das ist ja klein weil>> nur Zeiger drin?>> Spricht da was dagegen?>> Nicht das es generell sinnvoll ist, aber vielleicht kann DataObject auch> im Aufrufer erzeugt werden und getData() wird ein fillData(*obj) oder> initData(*obj).
Ich würde davon abraten, sowas zu machen nur aus Performancegründen und
ohne überprüft zu haben dass es tatsächlich einen Vorteil bringt. Der
dadurch entstehende Code ist fehleranfälliger und weniger intuitiv als
sich einfach auf RVO zu verlassen, und nichtmal notwendigerweise
schneller (ich würde sogar schätzen, eher langsamer).
Achim S. schrieb:> Nicht das es generell sinnvoll ist, aber vielleicht kann DataObject auch> im Aufrufer erzeugt werden und getData() wird ein fillData(*obj) oder> initData(*obj).
Naja, der Old-School Weg war (vor 20 Jahren) ja eher, dass zu füllende
Objekt zu übergeben. Auch dabei muss kein Zeiger oder dynamisch
alloziierter Speicher verwendet werden. DataObject müsste dabei
allerdings einen default c'tor haben:
Torsten R. schrieb:> Rules of Optimization (http://wiki.c2.com/?RulesOfOptimization):
...
> Und deswegen schlägst Du jetzt vor, premature optimization zu betreiben?
So ein Blödsinn - das ist keine Optimierung, sondern eine Frage eines
gesunden Stils. Wenn erstmal angefangen hat, ohne Sinn und Verstand
Objekte hin und her zu kopieren kann man später die App neu schreiben...
Man sollte schon wissen, WAS man eigentlich getan werden soll - kopiert
oder nicht. Das Wissen und Ausdruck dessen ist KEINE Optimierung,
sondern ein Zeichen von Mündigkeit.
zer0 schrieb:> Wenn erstmal angefangen hat, ohne Sinn und Verstand> Objekte hin und her zu kopieren kann man später die App neu schreiben...> Man sollte schon wissen, WAS man eigentlich getan werden soll - kopiert> oder nicht.
Könnten wir bitte mal festhalten dass Sinn und Verstand im vorliegenden
Fall darin bestehen, zu wissen, dass der Compiler die Kopie _nicht
durchführt_? Und dass es deshalb gerade optimal ist, es so
aufzuschreiben wie der TO es ursprünglich machen wollte, nämlich
1
Foofunc(){
2
Foox;
3
x.bar=...
4
...
5
returnx;
6
}
7
8
constFoo&y=func();
Das ist zufällig auch die intuitivste und einfachste Variante. Jede
"Optimierung" macht hier alles nur schlechter.
Sven B. schrieb:> Könnten wir bitte mal festhalten dass Sinn und Verstand im vorliegenden> Fall darin bestehen, zu wissen, dass der Compiler die Kopie _nicht> durchführt_?
Welcher Compiler? Du musst es ja wissen. Ist richtig. Kann aber auch mal
sein, dass er das nicht wegoptimierten kann. Und dann?
Gerade als Anfänger: Erst einmal lernen mit Zeigern, Referenzen und
unmittelbaren Objekten richtig umzugehen. Sonst rettet es der nächste
Standard auch nicht mehr.
zer0 schrieb:> Sven B. schrieb:>> Könnten wir bitte mal festhalten dass Sinn und Verstand im vorliegenden>> Fall darin bestehen, zu wissen, dass der Compiler die Kopie _nicht>> durchführt_?>> Welcher Compiler?
...
JEDER Compiler auf der Welt. JEDER. Solange du nicht irgendwelchen ganz
skurrilen Kram benutzt, was der TO sicherlich nicht tut. MSVC, gcc,
clang und vergleichbare machen das seit Jahrzehnten.
Wie gesagt: die Optimierung ist so offensichtlich und so wichtig und die
Designer der Sprache wollen so sehr, dass sich ihre Anwender darauf
verlassen, dass sie jetzt sogar in den Standard eingebaut wurde.
Der Lerneffekt an dieser Stelle sollte sein: verlass' dich auf RVO. Mit
Pointern arbeiten kann man an anderer Stelle lernen.
Man sollte auch berücksichtigen, was der Aufrufer mit dem Objekt
anstellen möchte. Wenn es nach dem Erstellen an seinem Platz bleibt, bis
es zerstört wird, ist per Value zurückgeben am effizientesten.
Wenn es im Laufe des Programms seinen Besitzer wechseln soll, ist ein
Smartpointer günstiger. Wenn es immer nur einen Besitzer gibt
std::unique_ptr, wenn es mehrere Besitzer geben kann std::shared_ptr.
Im Zweifel ist std::unique_ptr die universelle Lösung. Er hat im
Vergleich zu einem manuellen new/delete keinen Overhead und der Aufrufer
kann ihn bei Bedarf immer noch recht effizient in einen std::shared_ptr
umwandeln.
zer0 schrieb:> Welcher Compiler? Du musst es ja wissen. Ist richtig. Kann aber auch mal> sein, dass er das nicht wegoptimierten kann. Und dann?
Die gleichen Argumente, dass es kaputte Compiler gibt und man nicht
immer in der Lage ist, diesen kaputten Compiler auszutauschen (und einem
ggf. optimale Performance ohne Einschalten des Optimizers wichtig ist),
hatte Peter doch schon gebracht. Hattest Du Dir meine Antwort darauf
durchgelesen?
> Gerade als Anfänger: Erst einmal lernen mit Zeigern, Referenzen und> unmittelbaren Objekten richtig umzugehen. Sonst rettet es der nächste> Standard auch nicht mehr.
Gerade Anfänger neigen dazu, Code durch unnötige (und/oder falsche)
Optimierungen schlecht lesbar und wartbar zu machen. Die sollen erst
einmal lernen, Software zu schreiben, die Fehlerfrei ist und macht, was
sie machen soll.
Ein klares Design läßt sich in der Regel immer gut optimieren. Der OP
hat sich jetzt für unique_ptr<> entschieden. Unter der sehr
wahrscheinlichen Annahme, dass sein Compiler nicht kaput ist, wird seine
Lösung schon mal langsammer sein, da sie eine zusätzliche Allokation und
zusätzliche Indirektionen enthält. Sollte sich später herausstellen,
dass shared_ptr<> die bessere Lösung gewesen wäre, hat er eine
Schnittstellenänderung. Sollte sich herausstellen, dass der Allokator
das Bottleneck ist, dann hat er noch eine Schnittstellenänderung.
Ich kann jedem Anfänger nur dazu raten, von solchen Optimierungen die
Finger zu lassen (ja, ich weis es juckt! ;-) und die Software einfach
mal zu profilen (mit einem Profiler, nicht mit irgend welchen
Timestamps). Mir ist es noch nie gelungen, im Vorraus zu erraten, wo die
meiste CPU-Zeit verbraten wird (und nicht selten war der allocator
beteiligt).
Wenn Ihr schon Anfängern dazu ratet, smart pointer einzusetzen, habt Ihr
dann auch einen guten Tipp, woran sie erkennen, dass der Einsatz jetzt
geboten ist? Ab wann ist ein Objekt so groß, dass ein smart pointer
verwendet werden sollte? Wann ist es noch klein genug, um es direkt zu
kopieren/moven? Sollte man vorsichtshalber alle Rückgabewerte die
Objekttypen haben, mit smart pointern zurück geben?
Welchen smart pointer Typen sollte man als default verwenden. Oder
sollte man evtl. raw pointer verwenden und die Auswahl des geeigneten
smart pointer dem Aufrufer überlassen?
Fabian O. schrieb:> Wenn es im Laufe des Programms seinen Besitzer wechseln soll, ist ein> Smartpointer günstiger. Wenn es immer nur einen Besitzer gibt> std::unique_ptr, wenn es mehrere Besitzer geben kann std::shared_ptr.
Dann ist das return by value die unvierselle Lösung:
> Im Zweifel ist std::unique_ptr die universelle Lösung. Er hat im> Vergleich zu einem manuellen new/delete keinen Overhead und der Aufrufer> kann ihn bei Bedarf immer noch recht effizient in einen std::shared_ptr> umwandeln.
Es hat aber von vornhinein eine overhead gegenüber der einfachsten (und
auch performantesten) Lösung. Und dieses overhead bekommt man im
nachhinein dann auch nicht wieder weg.
Sven B. schrieb:> nd dass es deshalb gerade optimal ist, es so aufzuschreiben wie der TO> es ursprünglich machen wollte, nämlichFoo func() {> Foo x;> x.bar = ...> ...> return x;> }>> const Foo& y = func();>> Das ist zufällig auch die intuitivste und einfachste Variante.
Meiner Meinung nach ist diese Variante falsch. Es wird eine Referenz auf
ein nicht mehr existierendes Objekt angelegt. Ohne '&' ist es m.M.n.
richtig.
Dumdi D. schrieb:> Meiner Meinung nach ist diese Variante falsch. Es wird eine Referenz auf> ein nicht mehr existierendes Objekt angelegt. Ohne '&' ist es m.M.n.> richtig.
Nein, wenn man ein temporäres Objekt an eine const reference bindet,
verlängert sich die Lebenszeit des Objekts auf die Lebenszeit der
Referenz. Und das war auch schon immer so. Sonst könntest Du keine
Funktionen mit temporären Objekten aufrufen, wenn die den Parameter per
const ref nehmen.
Dumdi D. schrieb:> Jetzt erklaert mir aber wo das Objekt sich befindet? Wird es vom> Funktionenstackframe kopiert, oder per RVO im Aufruferstackframe> angelegt?
Der compiler erkennt ja zur Compilerzeit, dass er dort eine lokale
Variable mit dem selben scope, wie die Referenz anlegen muss. Ich würde
also zweiteres annehmen.
Der Grund, warum Sven das so geschrieben hat, ist ja: für den Fall, das
der Getter ein Objekt per value zurück gibt, ist der Code identisch zu
dem Code ohne Referenz. Wenn der Getter eine const reference auf ein
bestehendes Objekt zurück gibt, dann wird keine Kopie des Objekts
erstellt.
Torsten R. schrieb:> Fabian O. schrieb:>> Wenn es im Laufe des Programms seinen Besitzer wechseln soll, ist ein>> Smartpointer günstiger. Wenn es immer nur einen Besitzer gibt>> std::unique_ptr, wenn es mehrere Besitzer geben kann std::shared_ptr.>> Dann ist das return by value die unvierselle Lösung:> BigData factory();>> std::unique_ptr< BigData > unique = std::make_unique( factory() );> std::shared_ptr< BigData > unique = std::make_shared( factory() );>
Da wird zuerst ein temporäres Objekt auf dem Stack erzeugt und dann per
Copy-Constructor ein neues auf dem Heap angelegt. RVO greift hier nicht.
Beispiel:
Torsten R. schrieb:> Gerade Anfänger neigen dazu, Code durch unnötige (und/oder falsche)> Optimierungen schlecht lesbar und wartbar zu machen.
Hmm - "schlechter wartbar". Als hätte ich darauf gewartet...
Mit der RVO legt man das Interface der Funktion nach außen hin fest
1
Dataf1();
2
voidf2(Data&);
Eine von beiden Funktionen sieht so aus, als kopierte sie Daten auf den
Stack, die andere schreibt die Daten dort hin, wo der Aufrufer sie haben
will.
Im Laufe der Entwicklung muss man bei Variante 1 immer schauen, ob die
RVO noch funktioniert. Schon ein einfaches
1
Dataf1(){
2
Datadata;
3
...
4
if(inconsistent(data))returnsomething_else();
5
returndata;
6
}
sorgt dafür, dass die RVO mit hoher Wahrscheinlichkeit auf einmal nicht
mehr funktioniert.
Aber da ist das Interface der Funktion ja schon auf "return-by-value"
festgelegt, weil der Compiler das ja immer wegoptimiert...!
Squishy-Code ist Code, den man möglichst schnell schreiben kann, die
Wartbarkeit steht auf einem anderen Blatt. Ein Squishy trieft vor
Zucker. Eigentlich ist der Zucker überhaupt das wichtigste in einem
Squishy...
zer0 schrieb:> Data f1() {> Data data;> ...> if(inconsistent(data)) return something_else();> return data;> }
Und jetzt zeig mir mal, wie dieses Beispiel bei
Da D. schrieb:> Und jetzt zeig mir mal, wie dieses Beispiel bei ... ohne> kopieren von Daten funktioniert?!?
Na, das überlasse ich Deinem Genie herauszufinden, wie das in den
Fällen, wo die Daten konsistent sind ohne Kopie funktionieren kann. Man
könnte meinen, dann gibt es überhaupt kein Problem.
zer0 schrieb:> Mit der RVO legt man das Interface der Funktion nach außen hin fest> Data f1();> void f2(Data&);> Eine von beiden Funktionen sieht so aus, als kopierte sie Daten auf den> Stack, die andere schreibt die Daten dort hin, wo der Aufrufer sie haben> will.
f1 ist eine Funktion, die ein Objekt vom Typ Data zurückliefert.
1
autod=f1();
Vielen Danke f1!
f2 ist eine Funktion, die von mir ein Objekt vom Typ Data erwartet und
es möglicherweise irgendwie ändert.
1
Datad;
2
f2(d);
Mir ist f1 in den meisten Fällen deutlich lieber. Vor allem wenn es
eigentlich
ist.
Zusätzlich funktioniert f2 nur, wenn es für Data einen sinnvollen
default ctor gibt. Wenn es keinen logisch "leeren" Zustand gibt der
günstig erzeugt werden kann und man auf Biegen und Brechen einen solchen
Zustand erzwingen muss hat man sich mit f2 eine Menge Probleme erzeugt,
nur um möglicherweise ein paar move zu sparen.
Hallo,
zer0 schrieb:> Data f1();> void f2(Data&);> Eine von beiden Funktionen sieht so aus, als kopierte sie Daten auf den> Stack, die andere schreibt die Daten dort hin, wo der Aufrufer sie haben> will.
ja, die zweite Variante hatte ich auch, als "Old School" als "Lösung"
angeboten (ließ mal weiter oben). Sie hat halt andere Nachteile, die ich
als deutlich schwerwiegender und vor allem auch deutlich
wartungsunfreundlicher ansehe:
1) Data muss einen default c'tor haben. Gibt es keinen natürlichen,
kommt wieder das 'valid' Flag zur Lösung. Willkommen im Land der
Zombies! :-)
1
data d;
2
assert( !d.valid() );
3
f2( d );
4
assert( d.valid() );
Was passiert, wenn ich f2() mit dem selben d aufrufe? Habe ich die Daten
dann doppelt? Das Interface ist unscharf.
2) f2() kann nicht in Ausdrücken verwendet werden. Das bedeute vor
allem, dass ich d nicht als Konstante deklarieren kann. Damit muss ich
bei Lesen des Codes immer gucken, ob es irgendwo Änderungen an d gibt:
1
data d;
2
f2( d );
3
f3( d );
Ändert f3() d?
Dagegen:
1
const data d = f1();
2
// d ist valid, da data keinen c'tor hat, der einen ungültigen Zustand hinterläßt.
3
4
f3( d );
5
// keine Änderung an d möglich
6
7
f3( f1() );
8
// erst recht keine Änderung möglich.
9
10
f3( a ? f1() : f11() );
Das Du aus a = b; ein Kopieren machst, liegt an Deiner Erfahrung mit
C++. Die Semantik ist aber Zuweisung (bzw. Initialisierung).
> Im Laufe der Entwicklung muss man bei Variante 1 immer schauen, ob die> RVO noch funktioniert.
Ja, dass kann passieren. Ich fänd' das deutlich unspektakulärer, als die
Nachteile, die ich gerade aufgezählt habe. Zumal die Lösung für das
Problem dann ja so einfach ist (s.o.).
> Aber da ist das Interface der Funktion ja schon auf "return-by-value"> festgelegt, weil der Compiler das ja immer wegoptimiert...!
Ja, und das Problem ist total einfach zu lösen: Gibt dem Objekt move
Semantik oder nutze das Body-Handle idiom.
> Squishy-Code ist Code, den man möglichst schnell schreiben kann, die> Wartbarkeit steht auf einem anderen Blatt.
Wartbarkeit ist auch Lesbarkeit. Jede Warungsarbeit fängt mit Lesen und
Verstehen an. Dazu kommen Interfaces, die einfach richtig und schwerer
falsch zu benutzen sind. Fehlende const correctness, Klassen mit
Zombie-Zuständen, nicht intuitive Schnittstellen lassen viel Raum für
Fehler.
Zusammenfassung: Sicher, die von Dir vorgeschlagene Lösung sollte jeder
SW-Entwickler, der in der Wartung tätig ist, schon mal gesehen haben.
Ich finde, die Nachteile, die man sich damit einkauft, überwiegen
deutlich der Gefahr, dass es bei Änderungen zu einer Performance
Regression kommt. Zumal es dafür eine einfache Lösung gibt, ohne das
Interface zu ändern und ohne die Nachteile Deiner Lösung.
mfg Torsten
mh schrieb:> Mir ist f1 in den meisten Fällen deutlich lieber. Vor allem wenn es> eigentlichData<std::vector<std::string>, std::unordered_map<int,> std::string>, ...> Data d;> f2(d);> ist.
Tja - wähle dein Gift. Gerade bei solchen Konstrukten mit auto wird dir
das sicher einiges an Freude bereiten, wenn du den Quelltext ca. 100 Mal
öfter lesen und halbwegs nachvollziehen musst, als daran herum zu
schreiben.
Torsten R. schrieb:> Wenn der Getter eine const reference auf ein> bestehendes Objekt zurück gibt, dann wird keine Kopie des Objekts> erstellt.
Und wenn er das Objekt by value zurück gibt in der Regel auch nicht. ;)
Torsten Robitzki schrieb:> Was passiert, wenn ich f2() mit dem selben d aufrufe? Habe ich die Daten> dann doppelt? Das Interface ist unscharf.
Tja, touché. Das müss(t)en die Namen der Funktion und/oder des
Parameters deutlich machen. Jedoch taugt dieses Interface auch, um die
Daten an beliebige Position zu schreiben, also nicht nur auf den Stack.
Deswegen ist
Torsten Robitzki schrieb:> Das Du aus a = b; ein Kopieren machst, liegt an Deiner Erfahrung mit> C++. Die Semantik ist aber Zuweisung (bzw. Initialisierung).
genau das wirklich meine Erfahrung. Wenn der Wert mal in eine andere
Datenstruktur wandern soll, ist das zumindest ein move. Um ehrlich zu
sein, bin ich es - da zeigt sich meine Erfahrung -, schon relativ
überdrüssig auch nur Copy-Konstruktoren für jeden Fitzel an Klasse zu
schreiben. Von moves will ich hier gar nicht reden! Da deklariere ich
den Vektor doch lieber mit Zeigern als Inhalt, auch wenn moves nur um
den Faktor 4 oder sowas langsamer wären - wenn überhaupt.
Torsten Robitzki schrieb:> Wartbarkeit ist auch Lesbarkeit. Jede Warungsarbeit fängt mit Lesen und> Verstehen an. Dazu kommen Interfaces, die einfach richtig und schwerer> falsch zu benutzen sind. Fehlende const correctness, Klassen mit> Zombie-Zuständen, nicht intuitive Schnittstellen lassen viel Raum für> Fehler.
Also Zombie-Fehler entdeckt man meiner Erfahrung nach i.d.R. relativ
schnell schon bei den ersten Debug-Runs. Ein auf return-per-value
festgelegtes Interface löst in der Tat das const-Problem. Allerdings
bleibt der Makel der moves, wenn sich der Use-Case der Funktion
erweitert und die Daten nicht auf dem Stack landen sollen.
Zusammenfassung: Ich sehe das genau anders herum. Wenn es absehbar ist,
dass eine Lösung auf längere Sicht die Performance drückt, um dem
unstillbaren Verlangen nach syntaktischem Zucker gerecht zu werden,
fällt meine Wahl anders aus.
Außerdem ist das NICHT meine Lösung. Ich müsste mir erst einmal
anschauen, was es mit Data so auf sich hat - dazu würde auch wohl
gehören, sich anzuschauen, ob es eine extra Factory-Funktion echt
braucht, ob die Klasse gemoved werden sollte und wie der Use-Case so
aussieht. f2(Data&) kann auch alte Data-Objekte recyclen oder
ergänzen... :)
Heiko L. schrieb:> Um ehrlich zu> sein, bin ich es - da zeigt sich meine Erfahrung -, schon relativ> überdrüssig auch nur Copy-Konstruktoren für jeden Fitzel an Klasse zu> schreiben.
Was schreibst du für Klassen, dass du für "jeden Fitzel" nen copy-ctor
schreiben musst? Das kann man in den meisten Fällen dem Compiler
überlassen. Nur in Fällen, die direkt Ressourcen verwalten, müssen die
speziellen Memberfunktionen selbst geschrieben werden.
mh schrieb:> Was schreibst du für Klassen, dass du für "jeden Fitzel" nen copy-ctor> schreiben musst? Das kann man in den meisten Fällen dem Compiler> überlassen. Nur in Fällen, die direkt Ressourcen verwalten, müssen die> speziellen Memberfunktionen selbst geschrieben werden.
Wenn man Interfaces aus anderen Libraries verwendet, hat man den Fall
u.U. schon relativ schnell. Da gibt's dann clone() statt
Copy-Konstruktoren. C++-ismen funktionieren halt nicht über Compiler-
und stl-Grenzen hinweg.
Heiko L. schrieb:> Wenn man Interfaces aus anderen Libraries verwendet, hat man den Fall> u.U. schon relativ schnell. Da gibt's dann clone() statt> Copy-Konstruktoren.
Ok, wenn man für das Interface eine konkrete Klasse implementiert, muss
man die clone Methode schreiben. Aber, wenn man Objekte über das
Interface benutzt, kann man wieder den Compiler arbeiten lassen,
vorausgesetzt man hat einmal einen geeigneten Smart Pointer geschrieben,
der per clone kopiert. Oder habe ich etwas falsch verstanden?
mh schrieb:> Aber, wenn man Objekte über das> Interface benutzt, kann man wieder den Compiler arbeiten lassen,> vorausgesetzt man hat einmal einen geeigneten Smart Pointer geschrieben,> der per clone kopiert. Oder habe ich etwas falsch verstanden?
Nee, wie kommst du drauf? Da hast du das Clone sauber gelöst. Für
interne Objekte ist das ganze auch kaum ein Problem. Aber aus Libraries
erhält man typischerweise ein ganzes Sammelsurium an Kopierfunktionen -
gerade aus C-Libs, die es eigentlich gar nicht geben dürfte. Da ist es
schon Glück, wenn man für seine Wrapper-Klasse keinen Copy-Constructor
schreiben muss. Move ist dann wiederum trivial.
Der grundsätzliche Punkt war aber: Wenn man z.B. einen Vektor
deklariert, der u.U. mit der Zeit wächst, sollte man die Objekte
tendenziell NICHT by value speichern. Auch wenn es jetzt moves gibt.
DAS erspart einem die Arbeit prinzipiell.
Heiko L. schrieb:> Nee, wie kommst du drauf? Da hast du das Clone sauber gelöst. Für> interne Objekte ist das ganze auch kaum ein Problem. Aber aus Libraries> erhält man typischerweise ein ganzes Sammelsurium an Kopierfunktionen -> gerade aus C-Libs, die es eigentlich gar nicht geben dürfte. Da ist es> schon Glück, wenn man für seine Wrapper-Klasse keinen Copy-Constructor> schreiben muss. Move ist dann wiederum trivial.
Ok, "wie schreibe ich einen Wrapper für C-Libs" hat wenig zu tun mit
"wie schreibe ich C++ gute Klassen" ;-)
Heiko L. schrieb:> Der grundsätzliche Punkt war aber: Wenn man z.B. einen Vektor> deklariert, der u.U. mit der Zeit wächst, sollte man die Objekte> tendenziell NICHT by value speichern. Auch wenn es jetzt moves gibt.> DAS erspart einem die Arbeit prinzipiell.
Da sind wir jetzt wieder beim Optimieren. Wie groß sind die Objekte? Wie
teuer ist es die Objekte zu kopieren, statt einen Pointer? Ist das
Kopieren oder der indirekte Zugriff ein größeres Problem? Bekomme ich
Probleme, wenn ich viele evtl. kleine Objekte auf den Heap anlege? Ohne
genaue Infos und ohne es zu messen ist es nicht sinnvoll da pauschale
Aussagen zu treffen.
mh schrieb:> Da sind wir jetzt wieder beim Optimieren. Wie groß sind die Objekte? Wie> teuer ist es die Objekte zu kopieren, statt einen Pointer? Ist das> Kopieren oder der indirekte Zugriff ein größeres Problem? Bekomme ich> Probleme, wenn ich viele evtl. kleine Objekte auf den Heap anlege? Ohne> genaue Infos und ohne es zu messen ist es nicht sinnvoll da pauschale> Aussagen zu treffen.
Richtig, aber was ist da der Default und was die Optimierung? Bessere
Cache-Bursts oder Vermeidung von moves/copies? unique_ptr oder
move-Konstruktoren?
Optimieren wir das Beste oder vermeiden wir das Schlimmste?
Was mich vor allem wundert, ist, dass keiner der per-value-Fraktion sich
bisher nach der Größe des Data-Objekts erkundigt hat, weil es ja
"höchstens gemoved" wird...
Heiko L. schrieb:> Richtig, aber was ist da der Default und was die Optimierung? Bessere> Cache-Bursts oder Vermeidung von moves/copies? unique_ptr oder> move-Konstruktoren?
Für mich ist "per-value" der Default, weil mir dabei viel Arbeit
abgenommen wird. Aus weniger Arbeit folgen weniger Möglichkeiten für
Fehler. Und ich finde es generell lesbarer.
> Optimieren wir das Beste oder vermeiden wir das Schlimmste?
Weder noch. Solange ich nicht gemessen habe, weiß ich nicht, ob
per-value oder per-pointer das Beste oder das Schlimmste ist. In den
Fällen, in denen es wirklich offensichtlich ist, ist die Entscheidung
klar.
> Was mich vor allem wundert, ist, dass keiner der per-value-Fraktion sich> bisher nach der Größe des Data-Objekts erkundigt hat, weil es ja> "höchstens gemoved" wird...
Es kamen schon einige male "... ohne die Größe zu kennen ..." oder "...
kommt auf die Größe an ...". Die Erfahrung zeigt, dass man keine
zufriedenstellende Antwort bekommt, also stellt niemand explizit die
Frage.
mh schrieb:> Für mich ist "per-value" der Default, weil mir dabei viel Arbeit> abgenommen wird. Aus weniger Arbeit folgen weniger Möglichkeiten für> Fehler. Und ich finde es generell lesbarer.
Der Fragegehalt war: Raten wir jemandem, der mit einem völlig
unspezifiziertem Objekt ankommt dazu es ruhig per value zurück zu geben
und zu speichern oder nicht. Da ist da Frage, was im besten Fall besser
und was im schlimmsten weniger verheerend.
Heiko L. schrieb:> Was mich vor allem wundert, ist, dass keiner der per-value-Fraktion sich> bisher nach der Größe des Data-Objekts erkundigt hat, weil es ja> "höchstens gemoved" wird...
Ja, weil's völlig egal ist wie groß das ist im vorliegenden Fall.
Sven B. schrieb:> Heiko L. schrieb:>> Was mich vor allem wundert, ist, dass keiner der per-value-Fraktion sich>> bisher nach der Größe des Data-Objekts erkundigt hat, weil es ja>> "höchstens gemoved" wird...>> Ja, weil's völlig egal ist wie groß das ist im vorliegenden Fall.
Hmm, ich sehe den Quelltext nun nicht. Kann sein, kann auch nicht sein.
Ich würd nicht sagen, dass es völlig egal ist. Das Objekt könnte aus
einem std::array bestehen, dessen Größe den Stack gefährdet. ... Aber
wenn man der Argumentation weiter folgt, müsste man jeden einzelnen
Integer auf den Heap legen, weil der Stack ja schon randvoll sein
könnte. Aber jeder Pointer braucht Platzt auf dem Stack. Vllt. doch
keine gute Argumentation. ...
Heiko L. schrieb:> Da ist da Frage, was im besten Fall besser> und was im schlimmsten weniger verheerend.
Und das Problem bleibt, dass wir nicht wissen, was der beste Fall und
was schlimmste Fall ist. Und ohne die genauen Umstände zu kennen und
ohne es zu messen, werden wir es auch nie wissen. Also können wir
Performance nicht als Kriterium wählen. Und dann hat für mich per-value
mehr Vorteile als per-pointer.
mh schrieb:> Und das Problem bleibt, dass wir nicht wissen, was der beste Fall und> was schlimmste Fall ist.
Offensichtlich. Der schlimmste ist, die App wird saumäßig langsam. Der
beste ist: Der Quelltext sieht schöner aus.
Das ist nun wieder etwas, was ich nur verstehen kann. Man weiß, das ein
Konstrukt dauerhaft und a-priori jeden Datenmember im Speicher
verschieben muss und deswegen aus sich heraus die Performance schädigen
wird, weiß auch, dass sich eine Änderung des Interfaces dann im Falle
des Falles äußerst Aufwändig gestalten würde, denn einmal festgelegt,
wird diese Funktion so auch überall benutzt werden, aber es ist besser
lesbar, also entscheidet man sich dafür. Wo würdest du denn da die
Grenze ziehen?
1
voidf(stringreadOnlyString)
sieht ja auch viel schöner aus ohne const &. Wäre es "premature
optimization" den String per const & zu verlangen?
Heiko L. schrieb:> mh schrieb:>> Und das Problem bleibt, dass wir nicht wissen, was der beste Fall und>> was schlimmste Fall ist.>> Offensichtlich. Der schlimmste ist, die App wird saumäßig langsam.
Solange Du es nicht gemessen hast, weißt Du das nicht. Denn neben Donald
E. Knuth's Satz von der premature optimization gibt es ein weiteres
Gesetz in der Optimierung: "measure, don't guess" (Kirk Pepperdine).
> Der beste ist: Der Quelltext sieht schöner aus.
Im professionellen Umfeld wird ein enormer Aufwand getrieben, damit der
Code am Ende schöner aussieht, also: lesbar und verständlich ist. Das
solltest Du nicht einfach abtun, als wäre es nichts.
>
1
>voidf(stringreadOnlyString)
2
>
> sieht ja auch viel schöner aus ohne const &. Wäre es "premature> optimization" den String per const & zu verlangen?
Typsicherheit -- und dazu gehört Const Correctness im Entferntesten --
ist keine premature optimization, sondern saubere und korrekte
Programmierung. Daß der Compiler daraus Möglichkeiten zur Optimierung
ziehen kann, spielt dabei keine Rolle.
Sheeva P. schrieb:> Denn neben Donald> E. Knuth's
Knuth? Du meinst den Knuth, der über 3 Bücher lang Algorithmen in
Assembler ausbreitet und mathematische Beweise führt? Also, wie ich das
sehe, hat der dabei keine Daten unnötig durch die Gegend geschoben. Was
das nun optimiert?