Was passiert genau, wenn ich einen Point mit NULL initialisiere?
-
Damit du siehst, dass ich dir nicht bloß was erzähle:
Folgendes Programm:int main() { int data; cin>>data; cout<<data<<endl; }
gegen dieses:
class int_object { private: int data; public: friend ostream& operator<<(ostream &out, const int_object& in) { out<<in.data; return out; } friend istream& operator>>(istream &in, int_object& out) { in>>out.data; return in; } }; int main() { int_object data; cin>>data; cout<<data<<endl; }
Eines operiert direkt auf nativen Datentypen. Das andere operiert auf einem Klassenobjekt. Resultierender Maschinencode?
Dies für die erste Variante (gcc, -O3), relevanter Teil:.LFB1002: .cfi_startproc .cfi_personality 0x3,__gxx_personality_v0 pushq %rbp .cfi_def_cfa_offset 16 movl $_ZSt3cin, %edi pushq %rbx .cfi_def_cfa_offset 24 subq $24, %rsp .cfi_def_cfa_offset 48 leaq 12(%rsp), %rsi .cfi_offset 3, -24 .cfi_offset 6, -16 call _ZNSirsERi movl 12(%rsp), %esi movl $_ZSt4cout, %edi call _ZNSolsEi movq %rax, %rbx movq (%rax), %rax movq -24(%rax), %rax movq 240(%rbx,%rax), %rbp testq %rbp, %rbp je .L9 cmpb $0, 56(%rbp) je .L5 movzbl 67(%rbp), %eax .L6: movq %rbx, %rdi movsbl %al,%esi call _ZNSo3putEc movq %rax, %rdi call _ZNSo5flushEv addq $24, %rsp xorl %eax, %eax popq %rbx popq %rbp ret .p2align 4,,10 .p2align 3 .L5: movq %rbp, %rdi call _ZNKSt5ctypeIcE13_M_widen_initEv movq (%rbp), %rax movl $10, %esi movq %rbp, %rdi call *48(%rax) jmp .L6 .L9: .p2align 4,,5 call _ZSt16__throw_bad_castv .cfi_endproc
Dies für die zweite Variante:
.globl main .type main, @function main: .LFB999: .cfi_startproc .cfi_personality 0x3,__gxx_personality_v0 pushq %rbx .cfi_def_cfa_offset 16 movl $_ZSt3cin, %edi subq $16, %rsp .cfi_def_cfa_offset 32 movq %rsp, %rsi .cfi_offset 3, -16 call _ZNSirsERi movl (%rsp), %esi movl $_ZSt4cout, %edi call _ZNSolsEi movq _ZSt4cout(%rip), %rax movq -24(%rax), %rax movq _ZSt4cout+240(%rax), %rbx testq %rbx, %rbx je .L9 cmpb $0, 56(%rbx) je .L5 movzbl 67(%rbx), %eax .L6: movsbl %al,%esi movl $_ZSt4cout, %edi call _ZNSo3putEc movq %rax, %rdi call _ZNSo5flushEv xorl %eax, %eax addq $16, %rsp popq %rbx ret .p2align 4,,10 .p2align 3 .L5: movq %rbx, %rdi call _ZNKSt5ctypeIcE13_M_widen_initEv movq (%rbx), %rax movl $10, %esi movq %rbx, %rdi call *48(%rax) jmp .L6 .L9: .p2align 4,,5 call _ZSt16__throw_bad_castv .cfi_endproc
Unterschied der beiden? Ein paar andere Adressen und Registernamen. Und ein leaq wurde durch ein movq ersetzt.
-
SeppJ, um 100%ig zu überzeugen müsstest du beweisen, dass alle im Umlauf befindlichen C++-Compiler effektlose Funktionsaufrufe unter allen Umständen wegoptimieren.
Edit: Wobei ich mich da einfach drauf verlasse und mir lieber über andere Dinge den Kopf zerbreche...^^
Edit %2: Und in diesem fall auch, dass sie immer inlinen, wenn das ergebnis kürzer oder gleich dem Funktionsaufruf wäre.
-
Wie wär's denn mit folgender Aussage:
"Mit fast an Sicherheit grenzender Wahrscheinlichkeit kostet sinnvolle Abstraktion keine Performance ist mit absoluter Sicherheit in allen anderen Belangen überlegen".
-
Man müsste auch noch beweisen, dass nicht irgendein Compiler zum Spaß Warteschleifen im Code einbaut, um Leute zu bestrafen, die nicht mit Objekten programmieren. Laut Standard dürften sie das.
Und in diesem fall auch, dass sie immer inlinen, wenn das ergebnis kürzer oder gleich dem Funktionsaufruf wäre.
Die Sache, die Paul Müller wohl nicht ganz verstanden hat, ist, dass es da gar keinen Funktionsaufruf gibt. Man zeige mir den (meinetwegen inline) Konstruktor-/Destruktoraufruf. Wenn der Konstruktor nichts aktiv macht, dann gibt's auch keinen Code der generiert und ausgeführt werden könnte. Das ist genauso als würde man im ersten Programm die Zeile
int data;
im Maschinencode suchen. Auch die gibt es nicht, weil sie einfach nichts macht, sondern eine reine Abstraktion durch die Sprache ist (wenn auch auf sehr viel niedrigerem Abstraktionsniveau als die Klasse im zweiten Beispiel).Die Zeilen
int data;
undint_objekt data;
kommen nur insofern in den Maschinencode, als dass der Compiler sie benutzt, um die Adressen im Code richtig auszurechnen.
-
SeppJ schrieb:
Paul Müller schrieb:
Selbst ein Anfänger würde nicht bestreiten wollen, dass ein geschriebener ctor eine Funktion ist.
Und der Anfänger läge damit falsch. Du auch. Seit wann ist Speicherfüllen ein Funktionsaufruf?
Bitte alles lesen und nicht nur die ergänzenden Worte.
Shade Of Mine schrieb:
Denn wenn man im Ctor 3h lang initialisiert, so muss man das ohne Ctor ja auch tun, sonst wäre der Code nicht äquivalent.
Seit wann geht es bei Code-Abstraktion um äquivalenz? Klassen sind für die Wiederverwendbarkeit in der Regel recht allgemein gehalten.
Außerdem dreht sich die Frage nicht darum, ob ein Objekt anstatt dem reinen Code mehr Laufzeit kostet, sondern ob ein Objekt zusätzlich zum Code Laufzeit kostet?SeppJ schrieb:
ein Beispiel
Gut hast du also ein Beispiel gefunden, welches die Kriteren von Shade Of Mine erfüllt.
SeppJ schrieb:
Die Sache, die Paul Müller wohl nicht ganz verstanden hat, ist, dass es da gar keinen Funktionsaufruf gibt.
Ich weiß was inline macht. Du gehst aber die ganze Zeit davon aus, dass die Klasse exakt den selben Code ausführt, den du auch so verwendet hättest. Und das ist nicht immer der Fall. Nebenbei unterschlägst du auch, das inlining nicht immer möglich ist. Im Falle eines ctors/dtors zwar eigentlich schon, aber ein Objekt hat ja auch noch Methoden.
-
Paul Müller schrieb:
Außerdem dreht sich die Frage nicht darum, ob ein Objekt anstatt dem reinen Code mehr Laufzeit kostet, sondern ob ein Objekt zusätzlich zum Code Laufzeit kostet?
Und das tut es nicht. 0. Niemals.
Und die eigentliche Frage war ja, ob ein blanker Pointer schneller oder langsamer ist als ein sicher gekapselter Autopointer. Ich wollte auch eigentlich einen simplen Autopointer als Beispiel bringen, aber das wurde dann zu lang, weil ich für sauberen Code auch noch Kopierkonstruktor & Co hätte implementieren müssen und für Demonstrationszwecke diese auch noch benutzen.
Gut hast du also ein Beispiel gefunden, welches die Kriteren von Shade Of Mine erfüllt.
Auch beliebige andere Objekte. Code der das gleiche macht, braucht immer gleich lang, egal ob als Klasse, Template oder sonstwas.
SeppJ schrieb:
Die Sache, die Paul Müller wohl nicht ganz verstanden hat, ist, dass es da gar keinen Funktionsaufruf gibt.
Ich weiß was inline macht.
Da ist nichts zu inlinen. Da ist gar kein Funktionsaufruf. Zeig mir den geinlinten Konstruktoraufruf im Maschinencode, wenn da einer ist.
Du gehst aber die ganze Zeit davon aus, dass die Klasse exakt den selben Code ausführt, den du auch so verwendet hättest. Und das ist nicht immer der Fall.
Wieso nicht?
Nebenbei unterschlägst du auch, das inlining nicht immer möglich ist. Im Falle eines ctors/dtors zwar eigentlich schon, aber ein Objekt hat ja auch noch Methoden.
Wieso nicht?
Du hast es ja eigentlich sehr einfach, du brauchst bloß ein Gegenbeispiel zu zeigen, bei dem eine (natürlich nicht absichtlich schlecht programmierte) Klasse auch nur einen Maschinenbefehl mehr benötigt als ein vergleichbarer Code ohne Klasse. Dein Zug.
edit: Ok, hier ein Sparpointer:
int main() { int *data=new int; cin>>*data; cout<<*data<<'\n'; delete data; data=0; // Optional, aber du willst es ja so. Wird hier ohnehin wegoptimiert }
Gegen:
class heap_int { private: int *data; heap_int operator=(heap_int){return *this;} heap_int(const heap_int&){} public: heap_int():data(new int){} ~heap_int(){delete data;} int& operator*(){return *data;} }; int main() { heap_int data; cin>>*data; cout<<*data<<'\n'; }
Vorteil der Klasse: Nachdem die Entwicklungsarbeit für die Klasse erst einmal geleistet wurde (wurde sie ja bereits in der Standardbibliothek), ist der Code Anwendungscode kürzer und sicherer. Ich kann das new und das delete gar nicht vergessen und auch gar nicht doppelt aufrufen.
Resultierender Maschinencode ist wieder (für alle praktischen Zwecke) identisch:
pushq %rbx .cfi_def_cfa_offset 16 movl $4, %edi subq $16, %rsp .cfi_def_cfa_offset 32 .cfi_offset 3, -16 call _Znwm movl $_ZSt3cin, %edi movq %rax, %rbx movq %rax, %rsi call _ZNSirsERi movl (%rbx), %esi movl $_ZSt4cout, %edi call _ZNSolsEi leaq 15(%rsp), %rsi movl $1, %edx movq %rax, %rdi movb $10, 15(%rsp) call _ZSt16__ostream_insertIcSt11char_traitsIcEERSt13basic_ostreamIT_T0_ES6_PKS3_l movq %rbx, %rdi call _ZdlPv xorl %eax, %eax addq $16, %rsp popq %rbx ret
pushq %rbx .cfi_def_cfa_offset 16 movl $4, %edi subq $32, %rsp .cfi_def_cfa_offset 48 .LEHB0: .cfi_offset 3, -16 call _Znwm .LEHE0: movl $_ZSt3cin, %edi movq %rax, %rsi movq %rax, %rbx .LEHB1: call _ZNSirsERi movl (%rbx), %esi movl $_ZSt4cout, %edi call _ZNSolsEi leaq 31(%rsp), %rsi movl $1, %edx movq %rax, %rdi movb $10, 31(%rsp) call _ZSt16__ostream_insertIcSt11char_traitsIcEERSt13basic_ostreamIT_T0_ES6_PKS3_l .LEHE1: movq %rbx, %rdi call _ZdlPv xorl %eax, %eax addq $32, %rsp popq %rbx ret
An den Kommentaren die eingefügt wurden sieht man den wahren Unterschied: Der Compiler hat im zweiten Fall mehr gearbeitet. Ja, hier gibt es einen Unterschied. Dies ist der Preis der Sicherheit: Compilezeit.
-
SeppJ schrieb:
Du hast es ja eigentlich sehr einfach, du brauchst bloß ein Gegenbeispiel zu zeigen, bei dem eine (natürlich nicht absichtlich schlecht programmierte) Klasse auch nur einen Maschinenbefehl mehr benötigt als ein vergleichbarer Code ohne Klasse. Dein Zug.
Ich spar mir jetzt mal das Assembly, weil der Diff doch etwas länger ist.
int main() { int * foo(new int); int * bar(foo); delete bar; return 0; }
#include <tr1/memory> int main() { std::tr1::shared_ptr<int> foo(new int); std::tr1::shared_ptr<int> bar(foo); return 0; }
-
Die Code sind meiner Meinung nach nicht miteinander vergleichbar!
-
Das ist jetzt aber ein Vergleich von Äpfeln mit.. (nein Birnen wären noch zu nahe dran..) UFO's.
/EDIT
Wenn man für dein gewünschtes Verhalten einen solchen Smart Pointer nimmt, dann hat man auch nichts anderes als schlechte Performance verdient.Vergleichbar wäre so etwas:
class my_wrapper { public: my_wrapper(int* p) : p_(p){} my_wrapper(my_wrapper const& other) : p_(other.p_){} void destroy() { delete p_; } private: int* p_; }; int main() { my_wrapper foo(new int); my_wrapper bar(foo); bar.destroy(); return 0; }
Gleiche Vorteile, gleiche Nachteile. Das sollte jetzt so ziemlich zu dem asm Code kompiliert werden, wie deine Version.
Obwohl ich schon auch der Meinung bin, dass Abstraktion kosten kann. Kommt aber auf die Programmierweise und den Compiler an. Dass man sich über solche Sachen keine Gedanken machen muss, wenn man High Level programmiert ist ja selbstverständlich.
-
Ich habe oben noch ein weiteres Beispiel, wo die Codes auch tatsächlich das gleiche machen, reineditiert. Das dein Vergleich hinkt, muss ich dir wohl nicht noch erklären. Ein shared_pointer macht ganz was anderes als ein roher Pointer. Wenn du die Referenzenzähler in deinem Vergleichscode auch noch einbaust und benutzt, dann kann man sie vergleichen. Vorher macht der zweite Code einfach mehr.
-
Warum soll das ein Vergleich von Äpfeln mit UFOs sein? Der Code macht das gleiche und darum geht es. Es war nicht gefragt die Aussage von SeppJ zu bekräftigen, indem ich eine Klasse suche, die genauso wenig Code wie ein roher Zeiger erzeugt.
-
Der Code von Drakon ist doch nicht mehr als ein schlechter Scherz, welchen Sinn versprichst du dir von deinem "Wrapper"? Also ein wenig Ernsthaftigkeit sollte schon dabei. Auch wenn ich es mit einem Extrembeispiel etwas auf die Spitze getrieben habe.
-
Paul Müller schrieb:
Der Code von Drakon ist doch nicht mehr als ein schlechter Scherz, welchen Sinn versprichst du dir von deinem "Wrapper"? Also ein wenig Ernsthaftigkeit sollte schon dabei. Auch wenn ich es mit einem Extrembeispiel etwas auf die Spitze getrieben habe.
Das ist nicht mehr als dein Code auch macht. Wenn du mehr/anderes Verhalten haben willst, dann wird natürlich auch mehr Code erzeugt. Dass das so nicht viel Sinn macht ist ja wohl klar, aber es ist Abstraktion, die (bei einem guten Compiler) nichts kostet.
Warum soll das ein Vergleich von Äpfeln mit UFOs sein? Der Code macht das gleiche und darum geht es. Es war nicht gefragt die Aussage von SeppJ zu bekräftigen, indem ich eine Klasse suche, die genauso wenig Code wie ein roher Zeiger erzeugt.
Er macht nicht ganz das gleiche, nein. Der Smart Pointer weiss was alles referenziert wird und man muss sich da nicht selbst darum kümmern.
Wenn du den Teil aus dem Smart Pointer rausnimmst wird das Beispiel genau gleich schnell sein. Das Beispiel von SeppJ oben illustriert das auch recht gut.
-
Paul Müller schrieb:
...Also ein wenig Ernsthaftigkeit sollte schon dabei...
Dann fasse dir bitte auch mal selbst an die Nase. Ein scoped_ptr oder auto_ptr käme ja wenigstens noch in die Funktionelle Nähe eines reinen Zeigers. Ein shared_ptr erfüllt aber wesentlich mehr als nur einen Zeiger zu wrappen, und ist dann sinnvoll wenn eine Referenzzählung nötig ist, weil nicht sicher gestellt werden kann welcher der verwendeten Stellen, die letzte ist.
Wenn muss man wirklich auch gleiches mit gleichen vergleichen. Und dann kostet OO und Templates nicht viel, und bringen zudem richtig eingesetzt auch zusätzliche Sicherheit und Wartbarkeit mit fast 0 Mehrkosten.
Nachtrag: Und wenn wir schon Äpfel mit Birnen vergleichen, kann ich ja auch gleich das Gegenbeispiel bringen. Ein Programmierer meinte mir gegenüber immer das die Standardbibliothek etc. soviel mehr kosten, und baute sich daher eine auf seine Fälle "optimierte" Fassung zurecht, die fast ausschließlich prozedural geschrieben war. Als ich irgendwann diesen Code benutzen musste, und einen Fehler darin fand, tauschte ich das spaßeshalber mal mit einen mehr an "Abstraktion" aus (Und unter Verwendung von Klassen und der Standardbibliothek). Nicht nur das der Code danach ungefähr ein Zehntel so groß (und verständlicher) war, lief er auch noch um ein Drittel schneller.
Geschwindigkeit ist nicht nur durch einzelne Codestellen, sondern auch in Kombination zu sehen. Und wenn gleichzeitig Wartungsaspekte hinzu kommen, habe ich in der Praxis nicht selten erlebt das lesbarer Code nicht selten sogar genauso schnell (oder gar schneller) als die Handoptimierten war.
-
Die Aufgabe war aber nicht Abstraktion so anzupassen, dass sie keinen zusätzlichen Code erzeugt, sondern dass sie sinnvoll ist.
Und bei deiner Abstraktion geht jeglicher Sinn verloren. Meine macht dahingehend Sinn, dass beide Zeiger gültig sind, so wie im Ursprungsprogramm gefordert. Und ich muss mich nicht selber um das delete oder bei dir destroy kümmern, wofür ich ja eigentlich hier die Abstraktion einsetze. Und zu guter Letzt ich ändere nicht einfach eine bestehende Klasse. Was auch ein Punkt von sinnvoller Abstraktion ist.
-
Paul Müller schrieb:
Die Aufgabe war aber nicht Abstraktion so anzupassen, dass sie keinen zusätzlichen Code erzeugt, sondern dass sie sinnvoll ist.
Sinnvoll und vergleichbar, was dem Vergleich reiner Zeiger gegenüber shared_ptr eindeutig widerspricht. Du wirst ja hoffentlich auch nicht für 50m Fußweg ein Auto verwenden.
Paul Müller schrieb:
Meine macht dahingehend Sinn, dass beide Zeiger gültig sind, so wie im Ursprungsprogramm gefordert.
Aber du nimmst absichtlich für den Fall ungeeignete Beispiele.
Paul Müller schrieb:
Und zu guter Letzt ich ändere nicht einfach eine bestehende Klasse. Was auch ein Punkt von sinnvoller Abstraktion ist.
Aber auch bei der Verwendung bestehender Klassen nimmt man die passenden. Wenn eine Schraube leicht locker ist, rufe ich ja auch nicht gleich den Fachmann an, sondern nehme einen Schraubenzieher zur Hand.
-
Deine Abstraktion ist auch nicht sehr sinnvoll. shared_ptr benutzt man nicht, um in einem Scope ein kurzlebiges Objekt zu verwalten, sondern um ein Objekt in multiplen Kontexten verwalten zu können. In dem Beispiel wäre ein scoped_ptr sinnvoll. Das würde auch wieder vergleichbaren ASM-Code erzeugen.
-
Es ging bei meinen Beispiel nur darum die Aussage von SeppJ zu widerlegen, dass die Nutzung von Klassen niemals Bloat nach sich zieht. Da hab einfach das erstbeste genommen, wo ich mir sicher war. Die extreme sehe ich eher als Unterstützung meiner Aussage. Dass der Code absolut keinen Sinn ergibt ist dabei nicht von Bedeutung.
-
Wenn Du für Deine Zeiger manuell eine Referenzzählung durchführen würdest, dann würde im Vergleich kein „bloat“ entstehen. Funktional wäre es dann auch vergleichbar. So ist Dein Beispiel einfach Nonsense.
-
Tachyon schrieb:
Deine Abstraktion ist auch nicht sehr sinnvoll.
Das war auch nicht gefordert. Es wurde nur nach einem Beispiel verlangt.
Tachyon schrieb:
In dem Beispiel wäre ein scoped_ptr sinnvoll.
Nein wäre er nicht, weil er nicht dupliziert werden kann. Ich will über den Sinn nicht streiten, der Sinn ist ein äquivalentes Code Verhalten. Es geht hier auch nicht um den shared_ptr, sondern um eine Klasse die bloat erzeugt und da ist der shared_ptr nunmal ein gutes Beispiel. Und auch ein praxisrelevantes Objekt.