Forum: PC-Programmierung Zeiger auf Objekt weitergeben


von c-noob (Gast)


Lesenswert?

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?

von Peter II (Gast)


Lesenswert?

c-noob schrieb:
> Wie kann man das denn sinnvoll lösen?

Wenn es C++ ist mit einem SmartPointer oder unique_ptr.

von Torsten R. (Firma: Torrox.de) (torstenrobitzki)


Lesenswert?

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

von Peter II (Gast)


Lesenswert?

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.

von c-noob (Gast)


Lesenswert?

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?

von Peter II (Gast)


Lesenswert?

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.

von Torsten R. (Firma: Torrox.de) (torstenrobitzki)


Lesenswert?

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)

von Torsten R. (Firma: Torrox.de) (torstenrobitzki)


Lesenswert?

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!

von Peter II (Gast)


Lesenswert?

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.

von Peter II (Gast)


Lesenswert?

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
1
PrintString( const std::string s ) { ... };

oder
1
PrintString( const std::string& s ) { ... };

ist auch nur eine Optimierung.

von Sven B. (scummos)


Lesenswert?

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.

von Peter II (Gast)


Lesenswert?

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.

von Torsten R. (Firma: Torrox.de) (torstenrobitzki)


Lesenswert?

Peter II schrieb:
> schreibt du
>
>
1
> PrintString( const std::string s ) { ... };
2
>
>
> oder
>
>
1
> PrintString( const std::string& s ) { ... };
2
>
>
> 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

von Peter II (Gast)


Lesenswert?

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.

von c-noob (Gast)


Lesenswert?

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.

von Torsten R. (Firma: Torrox.de) (torstenrobitzki)


Lesenswert?

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?

von Sven B. (scummos)


Lesenswert?

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

von Peter II (Gast)


Lesenswert?

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.

von Peter II (Gast)


Lesenswert?

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.

von Sven B. (scummos)


Lesenswert?

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.

von Peter II (Gast)


Lesenswert?

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.

von Peter II (Gast)


Lesenswert?

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.

von Sven B. (scummos)


Lesenswert?

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

von Torsten R. (Firma: Torrox.de) (torstenrobitzki)


Lesenswert?

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.

von Peter II (Gast)


Lesenswert?

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.

von A. S. (Gast)


Lesenswert?

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

von mh (Gast)


Lesenswert?

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.

von Sven B. (scummos)


Lesenswert?

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

von Torsten R. (Firma: Torrox.de) (torstenrobitzki)


Lesenswert?

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:
1
void myClass::getData(DataObject& data)
2
{
3
    ... füllen mit Daten usw.
4
}

Aufruf:
1
DataObject data;
2
myClass    factory;
3
4
factory.getData( data );

Aber: wozu?

von zer0 (Gast)


Lesenswert?

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.

von Sven B. (scummos)


Lesenswert?

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
Foo func() {
2
  Foo x;
3
  x.bar = ...
4
  ...
5
  return x;
6
}
7
8
const Foo& y = func();

Das ist zufällig auch die intuitivste und einfachste Variante. Jede 
"Optimierung" macht hier alles nur schlechter.

von zer0 (Gast)


Lesenswert?

Torsten R. schrieb:
> Aber: wozu?

Weil es per Definition genau das tut, was es soll. Nur leider kein 
Squishy-Code.

von zer0 (Gast)


Lesenswert?

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.

von zer0 (Gast)


Lesenswert?

Sven B. schrieb:
> Jede "Optimierung" macht hier alles nur schlechter.

Meine Rede. Premature Optimization

von Sven B. (scummos)


Lesenswert?

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.

: Bearbeitet durch User
Beitrag #5137368 wurde von einem Moderator gelöscht.
von Fabian O. (xfr)


Lesenswert?

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.

von Torsten R. (Firma: Torrox.de) (torstenrobitzki)


Lesenswert?

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?

von Torsten R. (Firma: Torrox.de) (torstenrobitzki)


Lesenswert?

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:
1
BigData factory();
2
3
std::unique_ptr< BigData > unique = std::make_unique( factory() );
4
std::shared_ptr< BigData > unique = std::make_shared( factory() );

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

von Dumdi D. (dumdidum)


Lesenswert?

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.

von Torsten R. (Firma: Torrox.de) (torstenrobitzki)


Lesenswert?

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.

von Dumdi D. (dumdidum)


Lesenswert?

Danke, wieder was gelernt.
https://herbsutter.com/2008/01/01/gotw-88-a-candidate-for-the-most-important-const/
Jetzt erklaert mir aber wo das Objekt sich befindet? Wird es vom 
Funktionenstackframe kopiert, oder per RVO im Aufruferstackframe 
angelegt?

von Torsten R. (Firma: Torrox.de) (torstenrobitzki)


Lesenswert?

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.

von Fabian O. (xfr)


Lesenswert?

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:
1
#include <memory>
2
3
struct BigData {
4
  int data[10000];
5
};
6
7
BigData factory_value();
8
std::unique_ptr<BigData> factory_unique();
9
10
void take_ownership(std::unique_ptr<BigData> p);
11
12
void test_value() {
13
  take_ownership(std::make_unique<BigData>(factory_value()));
14
}
15
16
void test_unique() {
17
  take_ownership(factory_unique());
18
}

Ergebnis:
https://godbolt.org/g/GwpVyW

von zer0 (Gast)


Lesenswert?

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
Data f1();
2
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.
Im Laufe der Entwicklung muss man bei Variante 1 immer schauen, ob die 
RVO noch funktioniert. Schon ein einfaches
1
Data f1() {
2
  Data data;
3
  ...
4
  if(inconsistent(data)) return something_else();
5
  return data;
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...

von Da D. (dieter)


Lesenswert?

zer0 schrieb:
> Data f1() {
>   Data data;
>   ...
>   if(inconsistent(data)) return something_else();
>   return data;
> }

Und jetzt zeig mir mal, wie dieses Beispiel bei
1
void f2(Data&);
 ohne kopieren von Daten funktioniert?!?

von zer0 (Gast)


Lesenswert?

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.

von mh (Gast)


Lesenswert?

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
auto d = f1();
Vielen Danke f1!

f2 ist eine Funktion, die von mir ein Objekt vom Typ Data erwartet und 
es möglicherweise irgendwie ändert.
1
Data d;
2
f2(d);

Mir ist f1 in den meisten Fällen deutlich lieber. Vor allem wenn es 
eigentlich
1
Data<std::vector<std::string>, std::unordered_map<int, std::string>, ...> Data d;
2
f2(d);
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.

von Torsten Robitzki (Gast)


Lesenswert?

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

von Heiko L. (zer0)


Lesenswert?

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.

von Sven B. (scummos)


Lesenswert?

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

von Heiko L. (zer0)


Lesenswert?

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

von mh (Gast)


Lesenswert?

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.

von Heiko L. (zer0)


Lesenswert?

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.

: Bearbeitet durch User
von mh (Gast)


Lesenswert?

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?

von Heiko L. (zer0)


Lesenswert?

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.

von mh (Gast)


Lesenswert?

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.

von Heiko L. (zer0)


Lesenswert?

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

: Bearbeitet durch User
von mh (Gast)


Lesenswert?

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.

von Heiko L. (zer0)


Lesenswert?

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.

: Bearbeitet durch User
von Sven B. (scummos)


Lesenswert?

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.

von Heiko L. (zer0)


Lesenswert?

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.

von mh (Gast)


Lesenswert?

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.

von Heiko L. (zer0)


Lesenswert?

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
void f(string readOnlyString)
sieht ja auch viel schöner aus ohne const &. Wäre es "premature 
optimization" den String per const & zu verlangen?

von Sheeva P. (sheevaplug)


Lesenswert?

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
> void f(string readOnlyString)
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.

von Heiko L. (zer0)


Lesenswert?

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?

Bitte melde dich an um einen Beitrag zu schreiben. Anmeldung ist kostenlos und dauert nur eine Minute.
Bestehender Account
Schon ein Account bei Google/GoogleMail? Keine Anmeldung erforderlich!
Mit Google-Account einloggen
Noch kein Account? Hier anmelden.