Forum: PC-Programmierung Codeoptimierung vom Compiler


von Piet (Gast)


Lesenswert?

Hallo und guten Abend!

ich bearbeite gerade das Buch 'Object-Oriented Programming in C++' von 
N.Josutis. Dabei ist mir aufgefallen, dass der Autor häufig auf (teils 
gravierende) Laufzeit Unterschiede hinweist. Weil mir für die C Welt im 
uc-Unterforum immer wieder klar wurde, dass der Compiler in der Regel 
nahezu perfekt optimiert und man sich eigentlich schon fast anstrengen 
muss um etwas zu finden wo das nicht so ist, bin ich ungläubig 
verunsichert.

Ich würde euch deswegen gerne dazu fragen. Als Beispiel mal folgendes. 
Es geht um einen Multiplikations-Operator für eine Klasse Bruch. 
Vielleicht kennen auch einige das Buch.
1
//version a
2
Fraction Fraction::operator * (Fraction f)
3
{
4
   Fraction result;
5
   result.numer = numer*f.numer;
6
   result.denom = denom*f.denom;
7
   return result;
8
}
9
10
//version b
11
Fraction Fraction::operator * (Fraction f)
12
{
13
   return Fraction(numer * f.numer, denom * f.denom);
14
}

Ist es wirklich so, dass der Comiler für version a den default 
KOnstruktor falsch aufruft, zuweist, kopiert und freigibt und nicht 
'merkt' worum es geht?
Da schrillen ja alle Alarmglocken, dass man vieeeel zu leicht das 
performance Potential, durch ungeschickte Programmierung verspielt.

Könnt ihr mir dazu was genaueres erklären? Vielen Dank!

von 2⁵ (Gast)


Lesenswert?

Vor wie viel Jahren wurde denn das Buch geschrieben? "Normalerweise" 
sollte ein aktueller C++ Compiler in beiden Fällen guten Code erzeugen. 
Schau dir mal den generierten Assebmler-Code an.

von MaWin O. (mawin_original)


Lesenswert?

Die modernen Compiler sind ziemlich gut und optimieren das 
wahrscheinlich in den selben Maschinencode. Aber nur, wenn der 
Konstruktor einfach ist und keine Nebeneffekte hat.

Das zweite Beispiel ist aber besser, weil es ganz einfach auch deutlich 
lesbarer ist.

von Klaus (Gast)


Lesenswert?

https://godbolt.org/

Kann ich dir nur empfehlen wenn du so etwas genauer nachvollziehen 
möchtest.

von Cppbert (Gast)


Lesenswert?

Ich wuerde dir gcc.godbolt empfehlen, und mir mal den assembler output 
vom gcc, clang und msvc anschauen

Beitrag #7290360 wurde von einem Moderator gelöscht.
von 2⁵ (Gast)


Lesenswert?

Man sollte wissen, dass der gcc nur dann halbwegs optimierten Code 
erzeugt, wenn man auch zumindest eine "kleine" Optimierungsstufe, wie 
-O, -Os oder -Og anwählt.

von Yalu X. (yalu) (Moderator)


Lesenswert?

Piet schrieb:
> Ist es wirklich so, dass der Comiler für version a den default
> KOnstruktor falsch aufruft,

Wieso "falsch"? Du sagst dem Compiler in Version a ja explizit, dass er
den Default-Konstruktor aufrufen soll.

Ob der Aufruf überflüssig ist, kann der Compiler nur dann feststellen,
wenn ihm beim Kompilieren von Fraction::operator* die Definition des
Default-Konstruktors bekannt ist. Da ist bspw. dann der Fall, wenn die
Defintion direkt in der Klassendeklaration steht. Den größten Überblick
bei der Optimierung bietet die Option -flto (link time optimization), da
sollte der Default-Konstruktor – egeal, wo er definiert ist – auf jeden
Fall als überflüssig erkannt werden, sofern er es tatsächlich ist.
Allerings kann -flto bei größeren Programmen viel Zeit kosten.

Aber auch wenn der Compiler viele Dinge optimiert, sollte man ihm nicht
mutwillig Steine in den Weg legen. Genau das tut Version a. Da sie
umständlicher ist und damit auch dem Leser des Quellcodes Steine in den
Weg legt, gibt es überhaupt keinen Grund, ihr den Vorzug vor Version b
zu geben.

: Bearbeitet durch Moderator
Beitrag #7290367 wurde von einem Moderator gelöscht.
von Ein Kommentar (Gast)


Lesenswert?

Augenblick mal - wieso gehst du davon aus, die beiden Varianten führen 
zum selben Ergebnis?

Sicherlich legt jeder zurechnungsfähige Programmierer die Konstruktoren 
so an, dass der 2. Konstruktor das selbe macht, wie Defaultkonstruktor 
und Zuweisung. Aber das kann jeder Programmierer machen, wie er will.

von Wilhelm M. (wimalopaan)


Lesenswert?

Piet schrieb:
> Ist es wirklich so, dass der Comiler für version a den default
> KOnstruktor falsch aufruft,

Er ruft ihn nicht falsch auf, sondern genau deswegen, weil Du ihm das 
sagst!

Allerdings sind die meisten heutigen Compiler in der Lage zu erkennen, 
dass DU dort einen Fehler gemacht hast, und produzieren tatsächlich 
optimalen Code gemäß der as-if Regel.

C++ ist eine Programmiersprache, in der man ziemlich genau sagen kann, 
was man will. Allerdings muss man das dann auch in den meisten Fällen 
tun.

von Wilhelm M. (wimalopaan)


Lesenswert?

Piet schrieb:
> ich bearbeite gerade das Buch 'Object-Oriented Programming in C++'

Aus dem Jahr 2002. Das ist 20-Jahre her! Da würde ich ein aktuelles Buch 
empfehlen, was mindestens auch C++17 behandelt.

von Piet (Gast)


Lesenswert?

Hi und erst einmal danke für die Antworten! Ich bin heute wegen Covid 
Infektion etwas unpässlich. Ich werde mir auf jden Fall noch einal 
fieberfrei den Thread durcharbeiten.

Jo, dass das Buch einiges auf dem Buckel hat ist mir erst jetzt 
aufgefallen. Das sah zemlich neu aus und relativ wenig ausgeliehen ;-)

Ich habe zusätzlich noch einen Gegner mit Visual Studio Code - wenn man 
das zum ersten mal installiert, ist das ein ziemlicher Voodoo....


>Allerdings muss man das dann auch in den meisten Fällen
>tun.

Das ist jetzt eine Aussage, die mir eher Angst einjagd - der Rest vom 
Thread hat mich erst mal entspannt.
Meine Vorgehensweise wird vermutlich erst mal so sein, meine eigenen 
Muster zusammen zu tragen und dann erst mal umzusetzen. Also zu meinem 
Beispiel einen Konstruktor vorsehen, der zu den Operatoren passt, die 
man vorsieht und den dann aufrufen.

Pointer vermeiden. Eigentlich nur wenn man Heap Speicher besorgt - sonst 
nimmt man, ggf. const, Referenzen.

Einen Konstruktor mit default Werten auszustatten ist einfacher zu 
warten, aber wird auch etwas langsamer laufen wenn man z.B. nur einen 
Parameter braucht. (das habe ich jedenfalls so auch aus dem o.g. Buch 
verstanden)

Wilhelm, kannst du mir ein paar Tips für do's und dont's geben? Oder 
hast du an nichts bestimmtes gedacht. Der Aufruf gilt natürlich allen.

Danke noch mal für die Teilnahme jetzt lege ih mich erst mal wieder hin 
:-)

von Piet (Gast)


Lesenswert?

>Allerdings muss man das dann auch in den meisten Fällen
>tun.

Wenn das an mich ging: weil das ja der Autor als Beispiel aufführt...

von Oliver S. (oliverso)


Lesenswert?

Piet schrieb:
> Einen Konstruktor mit default Werten auszustatten ist einfacher zu
> warten, aber wird auch etwas langsamer laufen wenn man z.B. nur einen
> Parameter braucht. (das habe ich jedenfalls so auch aus dem o.g. Buch
> verstanden)

Ganz ehrlich, wirf das Buch in den Ofen.

Bevor du auch nur einen Gedanken an „langsam“ oder „schnell“ 
verschwendest, konzentriere dich erst einmal auf die Grundlagen von C++.

Der Rest ergibt sich dann schon.

Oliver

: Bearbeitet durch User
von Wilhelm M. (wimalopaan)


Lesenswert?

Piet schrieb:
> Wilhelm, kannst du mir ein paar Tips für do's und dont's geben? Oder
> hast du an nichts bestimmtes gedacht. Der Aufruf gilt natürlich allen.

Du bist ja noch am Lernen.
In dieser Phase mach Dir bitte überhaupt keine Gedanken um 
(Mikro-)Optimierungen. Der vornehmste Job des Compilers ist Optimierung, 
und das machen die verdammt gut: besser als solche vermeintlichen 
Kniffe.

Trotzdem: gewöhne Dir an, genau das hinzuschreiben, was Du willst. In 
diesem Fall möchtest Du eben nicht erst ein Objekt standard-konstruieren 
und danach den Zustand setzen. Sondern Du willst es mit den richtigen 
Werten von Anfang an (dem richtigen Zustand) erzeugen. Dies ist in 
Deinem Fall möglich, also mache es auch so. Das der Compiler andernfalls 
trotzdem optimalen Code (bei -Os oder -O3) hast Du dem Compiler zu 
verdanken ;-)

von Udo K. (udok)


Lesenswert?

Yalu X. schrieb:
> Ob der Aufruf überflüssig ist, kann der Compiler nur dann feststellen,
> wenn ihm beim Kompilieren von Fraction::operator* die Definition des
> Default-Konstruktors bekannt ist. Da ist bspw. dann der Fall, wenn die
> Defintion direkt in der Klassendeklaration steht.

Im gezeigten Fall kann der Compiler den Aufruf vom default constructor 
immer wegoptimieren! Er legt die temporäre Variable "result" einfach 
gleich in den Rückgabewert.

Der Rückgabewert ist aus Compilersicht genauso wie der "this" Zeiger ein 
vesteckter Funktionsparameter, der die Addresse des Rückgabewertes 
enthält
(rcx = this, rdx = Adresse des Rückgabewertes, r8x = Adresse des ersten 
Funktionsarguments f).  Klassen und Strukturen werden aus Compilersicht 
immer als Adressen übergeben, und nie als "Value" wie es die C++ Syntax 
eigentlich impliziert.

Trotz der Optimierung ist der erzeugte Code im Fall ohne temporäre 
Variable deutlich kleiner (43 zu 68 Bytes im Falle von VS 2022).

Im Falle, dass der Compiler alles benötigte Inline hat, ist der Code 
gleich (23 Bytes mit VS 2022).  clang macht aber auch dann noch 
kleineren Code mit der Variante ohne temporäre Variable.

Gruss,
Udo

von Wilhelm M. (wimalopaan)


Lesenswert?

Udo K. schrieb:
> Yalu X. schrieb:
>> Ob der Aufruf überflüssig ist, kann der Compiler nur dann feststellen,
>> wenn ihm beim Kompilieren von Fraction::operator* die Definition des
>> Default-Konstruktors bekannt ist. Da ist bspw. dann der Fall, wenn die
>> Defintion direkt in der Klassendeklaration steht.
>
> Im gezeigten Fall kann der Compiler den Aufruf vom default constructor
> immer wegoptimieren! Er legt die temporäre Variable "result" einfach
> gleich in den Rückgabewert.

NRVO ist nicht verpflichtend: der Compiler darf das, aber er muss es 
nicht im Gegensatz zu mandatory-copy-elision.

> Klassen und Strukturen werden aus Compilersicht
> immer als Adressen übergeben, und nie als "Value" wie es die C++ Syntax
> eigentlich impliziert.

Klassen (Datentypen) werden eh nie übergeben, nur Objekte. Und Objekte, 
die so klein sind, dass sie in Register passen, werden auch so übergeben 
(isra).

von Udo K. (udok)


Lesenswert?

Wilhelm M. schrieb:
> Du bist ja noch am Lernen.
> In dieser Phase mach Dir bitte überhaupt keine Gedanken um
> (Mikro-)Optimierungen. Der vornehmste Job des Compilers ist Optimierung,
> und das machen die verdammt gut: besser als solche vermeintlichen
> Kniffe.

Gerade in der Phase würde ich mir darüber Gedanken machen, zumindest 
wenn du gerne effizient programmierst.  Sonst bist du sowieso mit Python 
deutlich schneller am Ziel.

Erstens verstehst du durch Anschauen vom Assembler Code, was der 
Compiler wirklich macht (gerade in modernem C++ ab 2011 ist der 
Assembler Code die schnellste Methode die esoterische Doku auf den Boden 
der Realität zu holen).  Dazu musst du den Asm Code auch nicht im Detail 
verstehen.

Zweitens gewöhnst du dir so effektives Programmieren an. Die 
Anfängerfehler wirst du sonst nie wieder los.

Es macht auch nichts, wenn du ein altes Buch nimmst.  Damals war C++ 
noch deutlich einfacher und logischer und leichter verstehbar, und die 
Grundlagen haben sich nicht geändert.  Die neueren Dinge ab 2011 kannst 
du später noch nachlesen. Dazu gibt es im Internet auch etliches, wie 
hier zum Beispiel:
https://www.youtube.com/playlist?list=PLHTh1InhhwT4TJaHBVWzvBOYhp27UO7mI

von Udo K. (udok)


Lesenswert?

Wilhelm M. schrieb:
>> Im gezeigten Fall kann der Compiler den Aufruf vom default constructor
>> immer wegoptimieren! Er legt die temporäre Variable "result" einfach
>> gleich in den Rückgabewert.
>
> NRVO ist nicht verpflichtend: der Compiler darf das, aber er muss es
> nicht im Gegensatz zu mandatory-copy-elision.

Was anderes habe ich auch nicht geschrieben.

>
>> Klassen und Strukturen werden aus Compilersicht
>> immer als Adressen übergeben, und nie als "Value" wie es die C++ Syntax
>> eigentlich impliziert.
>
> Klassen (Datentypen) werden eh nie übergeben, nur Objekte. Und Objekte,
> die so klein sind, dass sie in Register passen, werden auch so übergeben
> (isra).

Ja schon klar.   Unter Windows wird auch kleine Object nie in Registern 
übergeben.

von Wilhelm M. (wimalopaan)


Lesenswert?

Udo K. schrieb:
> Gerade in der Phase würde ich mir darüber Gedanken machen, zumindest
> wenn du gerne effizient programmierst.  Sonst bist du sowieso mit Python
> deutlich schneller am Ziel.

Aus meiner Sicht: nein.

Er sollte lernen, genau das hinzuschreiben, was er meint.

Dazu gehört am Anfang zu verstehen,
- was const bedeutet,
- was der Unterschied zwischen Zuweisung und Initialisierung ist,
- wie abstrakte Referenzen ausgedrückt werden (Unterschiede zwischen 
C++-Referenzen und C++-Zeigern (Zeigervariablen und Zeigerwerte))
- Symmetrie zwischen primitiven DT und UDT

Wie das im Assembler aussieht, ist erstmal vollkommen irrelevant und 
eine Überforderung des Anfängers. Ziel ist es erstmal, die in C++ 
gegebenen Konzepte richtig einzusetzen. Wenn man das tut, kann der 
Compiler seinen Job machen: optimieren.

Statt solche Mikro-Optimierungen anzufangen ist es viel wichtiger, die 
Eigenschaften von abstrakten Datenstrukturen zu verstehen und darüber 
hinaus die Auswirkungen der Implementierung dieser einschätzen zu 
können: zusammenhängende Allokation vs verteilter Allokation sowie 
Hashing (und die Auswirkungen bei modernen CPUs).

von Udo K. (udok)


Lesenswert?

> Er sollte lernen, genau das hinzuschreiben, was er meint.

Das nützt nur nichts, wenn der Compiler anderer Meinung ist :-)

Da gibt es eben verschiedene Herangehensweisen.
Der eine ist ein abstrakter Denker, der tut sich auf der Ebene die du 
beschreibst leicht.

Der Praktiker tut sich eventuell leichter, wenn er sieht, was der 
Compiler wirklich aus dem abstrakten Konstrukt macht.  Das bisschen 
Assembler ist einfach zu verstehen, es werden ja in der Regel nur eine 
Handvoll Register verändert, und der Compiler verwendet meist nicht mehr 
als ein Duzend Assembler Befehle.  Das C++ Typen-Geschwafel wird 
eliminiert, Der Source Code wird auf 10% eingestampft, und man sieht was 
wirklich passiert.

: Bearbeitet durch User
von Johann L. (gjlayde) Benutzerseite


Lesenswert?

Wilhelm M. schrieb:
> Objekte, die so klein sind, dass sie in Register passen, werden
> auch so übergeben (isra).

Obkjekte werden so übergeben, wie vom ABI festgelegt.  Egal wie viel 
oder wie wenig optimiert wird.

Optimierungen wie SRA (Scalar Replacement of Aggregates) ist eine 
Optimierung, die Komposite auf Bit-gleiche Skalare abbildet.  Dies 
erfordert aber, das die betreffende Funktion geclont wird, damit sie ein 
neues, besser optimierbares Interface bekommt.  Für extern sichtbare 
Funktionen ist dies jedoch nicht möglich, und in jedem Falle hält sich 
der Compiler ans ABI.

Wäre ja auch schön blöd, wenn das ABI von Optimierungen abhinge.

von Wilhelm M. (wimalopaan)


Lesenswert?

Johann L. schrieb:
> Wilhelm M. schrieb:
>> Objekte, die so klein sind, dass sie in Register passen, werden
>> auch so übergeben (isra).
>
> Optimierungen wie SRA (Scalar Replacement of Aggregates) ist eine
> Optimierung, die Komposite auf Bit-gleiche Skalare abbildet.  Dies
> erfordert aber, das die betreffende Funktion geclont wird, damit sie ein
> neues, besser optimierbares Interface bekommt.  Für extern sichtbare
> Funktionen ist dies jedoch nicht möglich, und in jedem Falle hält sich
> der Compiler ans ABI.

Es sein denn, wir haben templates.

von Rolf M. (rmagnus)


Lesenswert?

Warum sollte es bei Templates anders sein?

von Wilhelm M. (wimalopaan)


Lesenswert?

Rolf M. schrieb:
> Warum sollte es bei Templates anders sein?

Weil die Instanziierung eines Templates es ermöglicht, einen passenden 
clone zu erstellen, ohne das ein externes ABI verletzt wird.

Aber Johann wird das sicher besser erklären können ;-)

von Rolf M. (rmagnus)


Lesenswert?

Wilhelm M. schrieb:
> Rolf M. schrieb:
>> Warum sollte es bei Templates anders sein?
>
> Weil die Instanziierung eines Templates es ermöglicht, einen passenden
> clone zu erstellen, ohne das ein externes ABI verletzt wird.

Nein, denn man kann Templates auch explizit instanziieren und dann von 
einer anderen Übersetzungseinheit aus diese Instanzen nutzen. Das geht 
nur dann, wenn sie sich auch an ein ABI halten.

von Johann L. (gjlayde) Benutzerseite


Lesenswert?

Wilhelm M. schrieb:
> Rolf M. schrieb:
>> Warum sollte es bei Templates anders sein?
>
> Weil die Instanziierung eines Templates es ermöglicht, einen passenden
> clone zu erstellen, ohne das ein externes ABI verletzt wird.

Ob es sich lohnt, eine Funktion / Methode zu clonen, ist doch unabhängig 
davon, ob der Code in C++ ohne Templates steht, oder ob er durch 
Templates "erzeugt" wurde.

: Bearbeitet durch User
von Udo K. (udok)


Lesenswert?

> Warum sollte es bei Templates anders sein?

Um das mal auf Deutsch und in verständlichen Worten zu sagen:
Templates sind normalerweise inline Code, und inline Code muss sich an 
keine Aufrufkonventionen halten.
Aber damit wird das Programm nur etwas kleiner, Geschwindigkeit gewinnt 
man damit nicht unbedingt (*); der Stack funktioniert heutzutage wie ein 
grosses Registerfile zusätzlich zu den 16 normalen x64 Registern. Viel 
mehr gewinnt man wenn man nicht immer alles mit -O2 übersetzen würde. 
O1 macht meist kleineren und schnelleren Code wegen etwas weniger L1/L2 
Cache Bedarf (**).

(*)
Sieht man schön mit taktgenauen Messungen:  Funktionsaufrufe sind 
heutzutage gratis, sofern die Branchprediction richtig liegt.

(**)
Ich hatte da mal ganz schön geschaut, als ich einen AVL-Baum optimierte. 
Jeder Knoten eines AVL-Baumes hat einen Zeiger auf den linken und auf 
den rechten Teilbaum, sowie ein Flag, das sagt welcher Teilbaum tiefer 
ist.  Der neue Code hat das Flag Feld (mit zusätzlichen User-Flags) mit 
dem linken Zeiger "verheiratet", und die Knotenstruktur von 24 auf 16 
Bytes runtergebracht.
Der neue Code war ca. 50% grösser und richtig hässlich, mit viel 
Bitverwurschteln. Vom Bauchgefühl her hätte ich gesagt: mindestens 100% 
langsamer.
Aber die neue Datenstruktur für jeden Knoten war deutlich kleiner.  Und 
das ist die Erklärung, warum der Benchmark schneller durchlief.

: Bearbeitet durch User
von Wilhelm M. (wimalopaan)


Lesenswert?

Johann L. schrieb:
> Ob es sich lohnt, eine Funktion / Methode zu clonen, ist doch unabhängig
> davon, ob der Code in C++ ohne Templates steht, oder ob er durch
> Templates "erzeugt" wurde.

Ich denke, es liegt daran, dass templates grundsätzlich inline sind 
(ausgenommen vollständige Spezialisierungen).

von Wilhelm M. (wimalopaan)


Lesenswert?

Udo K. schrieb:
> Ich hatte da mal ganz schön geschaut, als ich einen AVL-Baum optimierte.
> Jeder Knoten eines AVL-Baumes hat einen Zeiger auf den linken und auf
> den rechten Teilbaum, sowie ein Flag, das sagt welcher Teilbaum tiefer
> ist.

Das ist derselbe Effekte, warum ein std::vector<> oft wesentlich 
schneller ist als std::list<>, wenn der std::vector<> mit seinen Daten 
komplett in den Cache passt.

von Rolf M. (rmagnus)


Lesenswert?

Udo K. schrieb:
>> Warum sollte es bei Templates anders sein?
>
> Um das mal auf Deutsch und in verständlichen Worten zu sagen:
> Templates sind normalerweise inline Code, und inline Code muss sich an
> keine Aufrufkonventionen halten.

Das ist aber einerseits nicht immer so, und andererseits gibt es auch 
nicht-Templates, die inline sein können. Das ist also keine Eigenschaft, 
die spezifisch für Templates wäre.

> Sieht man schön mit taktgenauen Messungen:  Funktionsaufrufe sind
> heutzutage gratis, sofern die Branchprediction richtig liegt.

Funktionsaufrufe haben doch nichts mit branch prediction zu tun.

von Piet (Gast)


Lesenswert?

guten Morgen!

Wow und vielen Dank für die Rege Teilnahme am Thread. Ich muss gestehen, 
dass ich inhaltlich gegen Ende nicht alles verstehe, aber das sehe ich 
mal als positiv an. Es gibt viel zum weiter verfolgen.

Es hat sich bei mir noch begriffliche Verwirrung breit gemacht: 
"composition" und "Vererbung". Das sind Stichworte die mir über den Weg 
gelaufen sind und eine mögliche Falle in die ich als Anfänger offenbar 
laufen könnte, ist es mit der Brechstange alles zu vererben. nur - was 
ist denn unter "composition" eigentlich zu verstehen?

Grüße und bleibt fit zu Weihnachten!

von Udo K. (udok)


Lesenswert?

Rolf M. schrieb:
>>> Warum sollte es bei Templates anders sein?
>>
>> Um das mal auf Deutsch und in verständlichen Worten zu sagen:
>> Templates sind normalerweise inline Code, und inline Code muss sich an
>> keine Aufrufkonventionen halten.
>
> Das ist aber einerseits nicht immer so, und andererseits gibt es auch
> nicht-Templates, die inline sein können. Das ist also keine Eigenschaft,
> die spezifisch für Templates wäre.
>

Das ist richtig.  Drum habe ich "normalerweise" geschrieben.

>> Sieht man schön mit taktgenauen Messungen:  Funktionsaufrufe sind
>> heutzutage gratis, sofern die Branchprediction richtig liegt.
>
> Funktionsaufrufe haben doch nichts mit branch prediction zu tun.

Funktionsaufrufe sind auch nur Sprünge zu anderen Codeteilen.
Interessant ist der Fall, wo man "if (bedingung) func1()" hat.

von Udo K. (udok)


Lesenswert?

Piet schrieb:
> Wow und vielen Dank für die Rege Teilnahme am Thread. Ich muss gestehen,
> dass ich inhaltlich gegen Ende nicht alles verstehe, aber das sehe ich
> mal als positiv an. Es gibt viel zum weiter verfolgen.

Mir geht es auch nicht anders :-)

> Es hat sich bei mir noch begriffliche Verwirrung breit gemacht:
> "composition" und "Vererbung". Das sind Stichworte die mir über den Weg
> gelaufen sind und eine mögliche Falle in die ich als Anfänger offenbar
> laufen könnte, ist es mit der Brechstange alles zu vererben. nur - was
> ist denn unter "composition" eigentlich zu verstehen?

Diesen Fehler machen viele.  Die Realität ist aber eher ein Netz von 
Beziehungen als eine lineare Kette.  Das führt dann zu richtig 
hässlichem Code, wenn man alles ins Vererbungsschema reinpresst.  In C++ 
weiss dann keiner mehr, wo und wann was ausgeführt wird.  Wenn man sich 
aus dem Wust an Konstruktoren und Destruktoren noch raussieht, dann 
sorgen überladene Operatoren und Exceptions dafür, dass der Überblick 
verloren geht. Und Template-Metaprogrammierung verstehen dann nur mehr 
ein paar Duzend weltweit. C++ ist was für Vollzeitprogrammierer, aber 
hat enorm viel Lern-Overhead für Teilzeitprogrammierer.  Wenn nicht die 
vielen guten Libs wären, würde ich es einfach links liegen lassen, und C 
oder Javascript oder Python verwenden.

Was genau Composition heisst - Keine Ahnung, das müsste dir ein 
Informatiker erklären.  Klingt aber nach "Zusammensetzung" von Objekten. 
Vielleicht sowas:

Zusammensetzung:
1
struct A {
2
  struct B m_b;
3
  struct C m_c;
4
  int m_i;
5
};

Vererbung:
1
struct A : public struct B, public struct C
2
{
3
  int m_i;
4
};

: Bearbeitet durch User
von Wilhelm M. (wimalopaan)


Lesenswert?

Piet schrieb:
> Das sind Stichworte die mir über den Weg
> gelaufen sind und eine mögliche Falle in die ich als Anfänger offenbar
> laufen könnte, ist es mit der Brechstange alles zu vererben. nur - was
> ist denn unter "composition" eigentlich zu verstehen?

Komposition:
1
class A {
2
};
3
class B {
4
};
5
class Compositum {
6
    A a;
7
    B b;
8
};

Mit der Brechstange Vererbung einzusetzen ist eh falsch, weil es viele 
Probleme gibt. Vererbung sollte möglichst nur gegen 
Schnittstellen(klassen) eingesetzt werden, was einige der Probleme 
behebt.

Aber dies ist ein Thema von OOD und recht umfangreich. Es braucht auch 
umfangreiche Beispiele, um wirklich alle Vor- und Nachteile des einen 
oder anderen Ansätze zu verstehen. Da kommen dann so Stichworte wie 
dependency-inversion-principle und dependency-injection, Liskovsches 
Substitutionsprinzip und auch ein paar (Gegen-)beispiel wie etwa das 
Ellipse-Kreis-Problem.

Kauf Dir ein Buch zu OOD z.B. aus der "Head first"-Reihe. Es gibt auch 
ein paar schlechte Wikipedia-Artikel dazu.
Oder mache einen getrennten Thread hier dazu auf ;-) Es wird Antworten 
hageln ...

von Rolf M. (rmagnus)


Lesenswert?

Udo K. schrieb:
>>> Sieht man schön mit taktgenauen Messungen:  Funktionsaufrufe sind
>>> heutzutage gratis, sofern die Branchprediction richtig liegt.
>>
>> Funktionsaufrufe haben doch nichts mit branch prediction zu tun.
>
> Funktionsaufrufe sind auch nur Sprünge zu anderen Codeteilen.

Im Prinzip ja, aber keine bedingten Sprünge. Da gibt's nichts 
vorherzusehen. Der Sprung findet immer statt.

> Interessant ist der Fall, wo man "if (bedingung) func1()" hat.

Das sind dann aber zwei separate Dinge. Das eine ist ein bedingter 
Sprung für das if, das andere der Funktionsaufruf, der dann selbst keine 
Bedingung besitzt. Höchstens bei ARM kann das anders sein, weil da im 
Prinzip jeder Befehl bedingt ausgeführt werden kann.

von Udo K. (udok)


Lesenswert?

Ändert aber nichts daran, dass die Branchprediction falsch sein kann...

von MaWin O. (mawin_original)


Lesenswert?

Leute, Branchprediction und Mikrooptimierungen bringen euch Null komma 
Nix, wenn das Programm falsch ist, weil niemand es lesen und verstehen 
konnte.

Daher muss die Prio 1 vor Mikrooptimierungen immer die Lesbarkeit des 
Programms sein.

Und oft genug ist das am besten lesbare Programm auch das effizienteste.

Gerade als Anfänger ist es extrem wichtig das zu verstehen und zu 
verinnerlichen.

von Wilhelm M. (wimalopaan)


Lesenswert?

MaWin O. schrieb:
> Leute, Branchprediction und Mikrooptimierungen bringen euch Null komma
> Nix, wenn das Programm falsch ist, weil niemand es lesen und verstehen
> konnte.
>
> Daher muss die Prio 1 vor Mikrooptimierungen immer die Lesbarkeit des
> Programms sein.
>
> Und oft genug ist das am besten lesbare Programm auch das effizienteste.
>
> Gerade als Anfänger ist es extrem wichtig das zu verstehen und zu
> verinnerlichen.

Mein Reden: 
Beitrag "Re: Codeoptimierung vom Compiler"

von Rolf M. (rmagnus)


Lesenswert?

Udo K. schrieb:
> Ändert aber nichts daran, dass die Branchprediction falsch sein kann...

Sie kann nur bei Branches falsch sein, weil ohne Branch keine Branch 
Prediction! Und nur bedingte Sprünge sind Branches. Dass die bei einem 
if falsch liegen kann, stimmt schon, aber das hat rein gar nichts mit 
dem Funktionsaufruf zu tun.

von Wilhelm M. (wimalopaan)


Lesenswert?

Wilhelm M. schrieb:
> Komposition:
> class A {
> };
> class B {
> };
> class Compositum {
>     A a;
>     B b;
> };

Noch eine Bemerkung dazu, die ich vergessen hatte: eine Komposition ist 
- im Gegensatz zu einer Aggregation - ein existentielle 
Teil-Ganzes-Beziehung. Hier im Beispiel existieren die Elemente a und b 
so lange wie eine Objekt vom Typ Compositum (das könnte man auch mit 
std::unique_ptr<A> erreichen). Bei einer Aggregation muss das so nicht 
sein. In manchen Textbüchern wird auch noch darauf hingewiesen, dass die 
Aggregation meist homogen ist, die Komposition meist heterogen. Insofern 
ist die Komposition (in C++) etwas ganz normales.

Insgesamt finde ich diese Unterscheidung zwischen Komposition und 
Aggregation recht künstlich (wie auch viele andere Dinge, die so bei 
OOD/OOP betont werden).

von (prx) A. K. (prx)


Lesenswert?

Rolf M. schrieb:
> Sie kann nur bei Branches falsch sein, weil ohne Branch keine Branch
> Prediction! Und nur bedingte Sprünge sind Branches. Dass die bei einem
> if falsch liegen kann, stimmt schon, aber das hat rein gar nichts mit
> dem Funktionsaufruf zu tun.

Man muss zwischen der Vorhersage des Ausgangs bedingter Sprünge und der 
Vorhersage des Sprungziels aller ausgeführten Sprünge unterscheiden. 
Beide segeln unter dem Oberbegriff "branch prediction".

Anfangs wurden von Zieladressvorhersagen meist nur indirekte 
Sprünge/Calls sowie Returns erfasst, wenn überhaupt. Dank C++ und den 
darin häufigen virtual functions wurde das immer wichtiger, und 
beeinflusste die Entwicklung von Prozessoren. Es kann aber auch sein, 
dass die Zieladresse aller Sprünge vorhergesagt wird, ob direkt oder 
indirekt, weil das verfahrenstechnisch schneller ist, als eine später in 
der Pipeline stattfindende exakte Sprungzielberechnung.

Nur sind solche Gedanken etwas, das nicht am Anfang von üblicher 
Programmierung stehen sollte. Zumal die entsprechenden Mechanismen 
hochkomplex, prozessorspezifisch und schlecht dokumentiert sind, und 
teils sehr tief in die Implementierung der Prozessorarchitekturen hinein 
reichen.

: Bearbeitet durch User
von Piet (Gast)


Lesenswert?

vielen Danke Leute!


Wilhelm M. ich werde mich im nächsten Jahr bestimmt an den Rat eines 
OOD-Threads halten. Mit etwas Glück wird der ja ebenfalls so fruchtbar 
wie hier. (da ist es nach meiner Erfahrung immer entscheidend, welche 
user so als erstes antworten g)
C-Programme bzw. MEINE C Programme enden zumeist so, dass sie 
herrvorragend gemeint und gut begonnen sind und nach Monaten des 
Wachsens, Pflege und Bugfixings doch mehr oder weniger Sourcen bekommen, 
die sich über globale Variablen verzahnen. Das möchte ich natürlich 
verbessern. daher hab ich mir das originale Buch ausgeliehen gehabt.

Meine Inutuition sagt mir aber, das eine fast unlösbare Aufgabe ist in 
c++ etwas von Null an sinnvoll und zukunftssicher zu designen. ich habe 
auch schon mal Arduino Sourcen gesichtet, weil das ja doch ziemlich 
generalisiert scheint - da gibt es aber von den Kern Libraries zu den 
3rd Party Sachen den von mir beschriebenen Effekt zu sehen.

Daher: ja ich mache auf jeden Fall einen Thread in Richtung OOD bei dem 
ich (und andere ja natürlich auch) von Erfahrungen profitieren können.


Ein schönes Fest und einen guten Rutsch von meiner Seite aus.


Peter

von Wilhelm M. (wimalopaan)


Lesenswert?

Piet schrieb:
> Meine Inutuition sagt mir aber, das eine fast unlösbare Aufgabe ist in
> c++ etwas von Null an sinnvoll und zukunftssicher zu designen. ich habe
> auch schon mal Arduino Sourcen gesichtet, weil das ja doch ziemlich
> generalisiert scheint - da gibt es aber von den Kern Libraries zu den
> 3rd Party Sachen den von mir beschriebenen Effekt zu sehen.

Das liegt daran, dass diese Arduino-Sachen oft broken-by-design sind und 
Dir nicht als Vorbild dienen sollten.

von MaWin O. (mawin_original)


Lesenswert?

Piet schrieb:
> sinnvoll und zukunftssicher zu designen. ich habe
> auch schon mal Arduino Sourcen gesichtet

Die Arduino-Sourcen sind ein super Beispiel dafür, wie man es nicht 
machen sollte.

von Udo K. (udok)


Lesenswert?

Piet schrieb:
> C-Programme bzw. MEINE C Programme enden zumeist so, dass sie
> herrvorragend gemeint und gut begonnen sind und nach Monaten des
> Wachsens, Pflege und Bugfixings doch mehr oder weniger Sourcen bekommen,
> die sich über globale Variablen verzahnen. Das möchte ich natürlich
> verbessern. daher hab ich mir das originale Buch ausgeliehen gehabt.
>
> Meine Inutuition sagt mir aber, das eine fast unlösbare Aufgabe ist in
> c++ etwas von Null an sinnvoll und zukunftssicher zu designen.

Vergiss nicht, dass es keinen Unterschied macht, ob du globale Variablen 
verwendest, die dann irgendwann "irgendwie verzahnt" sind,
oder ob du C++ Klassen verwendest, die vielleicht am Heap allokiert 
sind, und deren Variablen dann "irgendwie verzahnt" sind.

Das Problem ist unabhängig von der Programmiersprache, und betrifft die 
Software Architektur.  Da kommst du mit einem Buch über 
Programmiersprachen nicht viel weiter. Die mir bekannten C++ Bücher sind 
eher kontraproduktiv, weil sie nur geeignete einfache Beispiele zeigen.

Eine passende Software Architektur hängt auch von der Größe des 
Entwicklerteams ab.
Ein grosses Entwicklerteam braucht viele gute Schnittstellen damit die 
Aufgabe in Module getrennt werden kann. Die Schnittstellen verdoppeln 
die Entwicklungszeit locker.
Für ein 1-Mann Team ist das eher hinderlich.

Meiner Erfahrung nach sind gute C Programme einfacher zu warten und zu 
lesen als C++ Programme.  Die C Compiler sind auch ausgereifter und 
stabiler.  Das spielt schon eine Rolle, wenn man als Hobby programmiert, 
und nicht die Manpower hat, Änderungen an der Entwicklungsumgebung 
mitzumachen.

Und dann gibt es wohl einfach Programme, die kompliziert sind und sich 
nicht klar strukturieren lassen.

Mir als Anwender ist es übrigens viel wichtiger, dass Programme gut 
funktionieren. Wie die intern arbeiten und ob die Strukturen "gut" sind 
ist wurscht.  Wichtiger sind mir ordentliche Fehlermeldungen und 
Behandlung von Randfällen.

Ich wünsche allen frohe Weihnachten & einen guten Rutsch ins neue Jahr,
Udo

von MaWin O. (mawin_original)


Lesenswert?

Udo K. schrieb:
> Die C Compiler sind auch ausgereifter und stabiler.

Ach komm.
So ein Unsinn.

Udo K. schrieb:
> Wie die intern arbeiten und ob die Strukturen "gut" sind
> ist wurscht.  Wichtiger sind mir ordentliche Fehlermeldungen und
> Behandlung von Randfällen.

Das eine beeinflusst aber das andere.
In Spaghetticode wird auch Behandlung von "Randfällen" immer 
schwieriger.

Udo K. schrieb:
> Vergiss nicht, dass es keinen Unterschied macht, ob du globale Variablen
> verwendest, die dann irgendwann "irgendwie verzahnt" sind,
> oder ob du C++ Klassen verwendest, die vielleicht am Heap allokiert
> sind, und deren Variablen dann "irgendwie verzahnt" sind.

Es ist aber deutlich einfacher globale Variablen versehentlich logisch 
zu "verzahnen", als die internen Datenstrukturen von Klasseninstanzen.
Auf globale Variablen kann man "einfach so" zugreifen.
Bei Klassen geht das nur über eine wohldefinierte API. (wer 
public-Elemente hat, hats dann auch nicht besser verdient)

von Yalu X. (yalu) (Moderator)


Lesenswert?

Piet schrieb:
> C-Programme bzw. MEINE C Programme enden zumeist so, dass sie
> herrvorragend gemeint und gut begonnen sind und nach Monaten des
> Wachsens, Pflege und Bugfixings doch mehr oder weniger Sourcen bekommen,
> die sich über globale Variablen verzahnen.

> Meine Inutuition sagt mir aber, das eine fast unlösbare Aufgabe ist in
> c++ etwas von Null an sinnvoll und zukunftssicher zu designen.

C und mehr noch C++ liefern dir gute Hilfsmittel für das Schreiben von
sauberem Code, lassen dir aber gleichzeitig auch sehr viele Freiheiten,
die u.a. für die Programmierung von Murks missbraucht werden können.
Deswegen braucht es nicht nur Disziplin, sondern auch viel Erfahrung, um
die verfügbaren Hilfsmittel sinnvoll einzusetzen und die Freiheiten mit
Bedacht nutzen.

Andere Programmiersprachen sind deutlich restriktiver und verhindern
oder erschweren gewisse Konstrukte und Architekturmerkmale, die
gemeinhin als schlechter Stil angesehen werden. Dies empfinden viele
Programmierer als Bevormundung, da sie der Compiler wegen jeder
Kleinigkeit wie ein strenger Lehrer anbrüllt. Auf der anderen Seite ist
diese Strenge ein Garant dafür, dass selbst Anfängercode, nachdem er
erst einmal die Hürden des Compilerprüfungen überwunden hat, eine
halbwegs ordentliche Qualität aufweist.

Eine solche Sprache ist bspw. Haskell. Globale, beschreibbare Variablen
sind in der Sprache gar nicht erst vorgesehen, weswegen auch die von dir
angesprochene Verzahnung nicht möglich ist. Falls man so etwas unbedingt
doch einmal braucht, gibt es dafür die Bibliothek Data.IORef, mittels
der das Beschreiben globaler Objekte sozusagen simuliert wird. Die
Anwendung von IORefs legt dem Programmierer aber immer noch genügend
Steine in den Weg, dass dieser es sich zweimal überlegt, bevor er sie
wirklich nutzt.

Auch eine lokal beschränkte Verzahnung innerhalb einer Gruppe weniger
Funktionen ist über eine Bibliothek (Data.STRef) möglich. Das ist ganz,
ganz grob mit den Membervariablen einer C++-Klasse vergleichbar, auf die
– falls als private deklariert – nur die Memberfunktionen der Klasse
zugreifen können. Aber auch hier gelten in Haskell starke Restriktionen,
so dass auch STRefs nur dann eingesetzt werden, wenn keine Alternativen
gibt.

Wenn du bei C++ die erfahreneren Experten fragen musst:

"Wie muss ich mein Programm strukturieren, damit guter Stil daraus
wird?"

musst du bei Haskell fragen:

"Wie muss ich mein Programm strukturieren, damit es vom Compiler
akzeptiert wird?"

Statt in einem Forum bei Experten nachzufragen, kannst du natürlich auch
selber überlegen, warum der Compiler meckert (dessen Fehlermeldungen
enthalten dazu meist wichtige Hinweise) und eigene Lösungsideen so lange
ausprobieren, bis der Compiler zufrieden ist. Das liefert oft einen
höheren Erkenntnisgewinn, als Vorschlägen aus dem Netz blind zu folgen.

> Ein schönes Fest und einen guten Rutsch von meiner Seite aus.

Danke, ebenfalls.

von Le X. (lex_91)


Lesenswert?

MaWin O. schrieb:
> wer public-Elemente hat, hats dann auch nicht besser verdient

Dachte ich auch lange, und dann kam Python und hat uns dazu gebracht 
alles Wegzuwerfen was wir in C++ als "guten Stil" gelernt haben.

von __ (Gast)


Lesenswert?

Le X. schrieb:
> Dachte ich auch lange, und dann kam Python und hat uns dazu gebracht
> alles Wegzuwerfen was wir in C++ als "guten Stil" gelernt haben.

__-Prefix kennste?

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.