Was passiert genau, wenn ich einen Point mit NULL initialisiere?



  • SeppJ schrieb:

    @Paul Müller: Das klingt alles so, als würdest du so mit Pointern programmieren, dass man darauf tatsächlich aufpassen müsste, d.h. einfach wild drauflos.

    Das hat mit meinen Programmierkünsten erstmal wenig zu tun. Da gibt es halt Code, den hat irgendjemand anderes mal geschrieben und da musst du jetzt was ran/rein basteln. Natürlich ist da nicht die Lebenszeit jedes einzelnen Pointers dokumentiert. Aber dafür gibt es umso mehr Pointer und dirty Tricks, die man mit Pointern machen kann.

    SeppJ schrieb:

    Idealerweise programmiert man so, dass solche Probleme gar nicht erst auftreten können.

    Das ist meist schwierig, wenn es um zeitkritische Aufgaben geht.

    SeppJ schrieb:

    allgemein: delete prüft ja ohnehin irgendwie, ob die Freigabe gültig ist.

    Im einfachsten Fall würdest du dann eine Exception bekommen, diese müsste du aber wieder gesondert behandeln. Und wie schon angemerkt, was heißt gültig, ein Speicherbereich kann auch wieder vergeben werden.

    SeppJ schrieb:

    Da ist es doch kein Problem, die 0 gesondert zu behandeln, ohne dass es merkliche Laufzeitverluste gibt.

    Es geht ja auch mehr darum definieren zu können, ob ein Zeiger gültig ist oder nicht. Von new und delete soll man ja auch die Finger lassen soweit es möglich ist.



  • Paul Müller schrieb:

    ...Von new und delete soll man ja auch die Finger lassen soweit es möglich ist...

    Das würde ich so nicht stehen lassen, auch wenn man sich fragen sollte ob man sich selbst um die Speicherverwaltung kümmern muss. Stackvariablen sind Heapvariablen zwar häufig vorzuziehen, nur ist der Stack zumeist begrenzt, und manches lässt sich auf dem Heap besser regeln.



  • Paul Müller schrieb:

    ...
    Ich würde sogar soweit gehen und delete einfach den Zeiger NULL setzen lassen. Aber das wird wahrscheinlich nicht den Weg in den Standard finden. Diese eine Anweisung tut keinem weh, im Verhältnis zum restlichen delete, aber würde soviel bringen.

    Bjarne Stroustrup schrieb:

    C++ explicitly allows an implementation of delete to zero out an lvalue operand, and I had hoped that implementations would do that, but that idea doesn't seem to have become popular with implementers.

    If you consider zeroing out pointers important, consider using a destroy function:

    template<class T> inline void destroy(T*& p) { delete p; p = 0; }

    Man beachte aber auch http://www2.research.att.com/~bs/bs_faq2.html#delete-zero



  • asc schrieb:

    Diese Aussage sehe ich als gefährlich an, da ich es durchaus von einigen Debuggern kenne das man uninitialisierte Zeiger im Debugmodus eben nicht bemerkt, da sie vom Debugger mit 0 initialisiert wurden, im Release aber ein Zufallswert in diesen steht.

    Ist ne ziemliche schwäche des Compilers dann.
    Der VC++ setzt zB uninitialisierte Zeiger auf 0xCDCDCDCD

    Und jeder Zugriff auf diese Adresse ist sofort ein breakpoint.



  • asc schrieb:

    Das würde ich so nicht stehen lassen, auch wenn man sich fragen sollte ob man sich selbst um die Speicherverwaltung kümmern muss.

    Darauf wollte ich jetzt gar nicht hinaus. Mir ging es um die Zeit, die ein new/delete verbrät. Deswegen sollten man damit lieber sparsam umgehen. Objekte verschlimmern diese ganze Angelegenheit auch nur.
    Das hängt aber auch stark von der Zielplattform ab. Auf einem normalen Desktop-Computer braucht man sich in der Regel keine Gedanken darum machen.


  • Mod

    Paul Müller schrieb:

    ...gefährliches Halbwissen...

    Tut mir leid, aber fast alles was du in deinen letzten beiden Beiträgen geschrieben hast ist Quatsch.



  • @SeppJ
    Was den genau? Ich kann im letzten Beitrag von mir keinen Unsinn entdecken. Maximal eine Pauschalisierung bzgl. ob man auf Desktops ressourcensparend programmieren sollten. Wenn ich dich aber richtig verstehe, würdest du die Aussage sogar ausweiten, anstatt einschränken wollen?


  • Mod

    Paul Müller schrieb:

    @SeppJ
    Was den genau? Ich kann im letzten Beitrag von mir keinen Unsinn entdecken. Maximal eine Pauschalisierung bzgl. ob man auf Desktops ressourcensparend programmieren sollten. Wenn ich dich aber richtig verstehe, würdest du die Aussage sogar ausweiten, anstatt einschränken wollen?

    Ok, hier eine detaillierte Analyse:

    Mir ging es um die Zeit, die ein new/delete verbrät. Deswegen sollten man damit lieber sparsam umgehen.

    Und was soll man stattdessen nehmen? Entweder braucht man Heapobjekte oder eben nicht. Grundlos new/delete benutzen meistens nur Leute die von Java kommen, weil sie es nicht anders kennen.

    Objekte verschlimmern diese ganze Angelegenheit auch nur.

    Unsinn. Abstraktion kostet keine Laufzeit.

    SeppJ schrieb:

    Idealerweise programmiert man so, dass solche Probleme gar nicht erst auftreten können.

    Das ist meist schwierig, wenn es um zeitkritische Aufgaben geht.

    Unsinn. Abstraktion kostet keine Laufzeit.

    SeppJ schrieb:

    allgemein: delete prüft ja ohnehin irgendwie, ob die Freigabe gültig ist.

    Im einfachsten Fall würdest du dann eine Exception bekommen, diese müsste du aber wieder gesondert behandeln.

    Was willst du mir damit sagen? Ich schildere nur den Sachverhalt. Es ging darum, ob das delete dadurch langsamer wird, weil es die 0 gesondert behandelt. Das wird es nicht wesentlich, weil beim delete sowieso irgendwelche Prüfungen durchgeführt werden.

    Und wie schon angemerkt, was heißt gültig, ein Speicherbereich kann auch wieder vergeben werden.

    Gültig heißt, dass es vorher ein passendes new gab. Speicher wird nicht wieder vergeben, bevor er nicht freigegeben wurde. Wenn du über mehrere Pointer auf den gleichen Speicherbereich zugreifst (so dass er zwischendurch freigegeben und wieder zugewiesen werden kann, ohne dass ein anderer Pointer es merkt), sind wir wieder im Bereich unsauberer Programmierung.

    Von new und delete soll man ja auch die Finger lassen soweit es möglich ist.

    Da haste Recht. Aber ich glaube die Gründe sind dir nicht so ganz klar.



  • SeppJ schrieb:

    Und was soll man stattdessen nehmen? Entweder braucht man Heapobjekte oder eben nicht.

    Man kann die Objekte auch einfach recyceln. Das man irgendwann mal ein Objekt erstellen muss steht dabei nicht zur Debatte.

    SeppJ schrieb:

    Unsinn. Abstraktion kostet keine Laufzeit.

    Du willst mir nicht ernsthaft erzählen, dass ein ctor/dtor Aufruf genauso viel Laufzeit benötigt wie kein Aufruf?

    SeppJ schrieb:

    Unsinn. Abstraktion kostet keine Laufzeit.

    Hier das gleiche. Du meinst nicht ernsthaft etwas zu machen geht genauso schnell, wie etwas nicht zu machen?

    SeppJ schrieb:

    Was willst du mir damit sagen? Ich schildere nur den Sachverhalt. Es ging darum, ob das delete dadurch langsamer wird, weil es die 0 gesondert behandelt. Das wird es nicht wesentlich, weil beim delete sowieso irgendwelche Prüfungen durchgeführt werden.

    Meinetwegen werden Prüfungen durchgeführt. Diese belaufen sich aber auf NULL, der Trivialfall. Und ob der Zeiger mit new allokiert wurde. Andernfalls gibts eine Exception, die du fangen müsstest und irgendwas damit anstellen oder dein Programm kracht komplett weg.
    Die mühselige Exception kann ich mir sparen, indem ich einfach den Pointer NULL setze. Und das hab ich zwar schon erwähnt aber ein delete zuviel tut nicht weh, nur ein delete zu wenig. Und wenn es nicht weh tut, ist es mir auch egal, ob man es als unsauber ansehen kann. Der Link von manni66 zeigt zwar, dass das nicht immer funktioniert, aber das kannst du ja in deinem Design berücksichtigen.

    SeppJ schrieb:

    Gültig heißt, dass es vorher ein passendes new gab. Speicher wird nicht wieder vergeben, bevor er nicht freigegeben wurde.

    Das hat auch niemand bestritten.

    SeppJ schrieb:

    Wenn du über mehrere Pointer auf den gleichen Speicherbereich zugreifst (so dass er zwischendurch freigegeben und wieder zugewiesen werden kann, ohne dass ein anderer Pointer es merkt), sind wir wieder im Bereich unsauberer Programmierung.

    Siehe meine Anmerkung zu manni66 seinem Link.

    SeppJ schrieb:

    Da haste Recht. Aber ich glaube die Gründe sind dir nicht so ganz klar.

    Ich denke schon. Ich finde es nur etwas merkwürdig das du bestreiten willst, dass ein Funktionsaufruf Laufzeit kostet. Ich denke die Bedeutung von inline-Funktionen ist dir vertraut?


  • Mod

    Paul Müller schrieb:

    SeppJ schrieb:

    Unsinn. Abstraktion kostet keine Laufzeit.

    Du willst mir nicht ernsthaft erzählen, dass ein ctor/dtor Aufruf genauso viel Laufzeit benötigt wie kein Aufruf?

    Du willst mir nicht ernsthaft erzählen, dass da tatsächlich eine Funktion aufgerufen wird.

    Die mühselige Exception kann ich mir sparen, indem ich einfach den Pointer NULL setze.

    Die Exception und alles andere kannst du dir gleich sparen, indem du vernünftig programmierst und nicht irgendwelche Zeiger löscht, von denen du nicht einmal weißt, wohin sie zeigen.

    Und das hab ich zwar schon erwähnt aber ein delete zuviel tut nicht weh, nur ein delete zu wenig.

    Wenn du ein delete zu viel machst, dann hat dein Programm einen Fehler. An anderer Stelle. Und du merkst es noch nicht einmal, wenn du immer blind alles nullst.

    SeppJ schrieb:

    Da haste Recht. Aber ich glaube die Gründe sind dir nicht so ganz klar.

    Ich denke schon. Ich finde es nur etwas merkwürdig das du bestreiten willst, dass ein Funktionsaufruf Laufzeit kostet. Ich denke die Bedeutung von inline-Funktionen ist dir vertraut?[/quote]Besser als dir offensichtlich.



  • SeppJ schrieb:

    Du willst mir nicht ernsthaft erzählen, dass da tatsächlich eine Funktion aufgerufen wird.

    Ich hoffe das soll ein Scherz sein. Selbst ein Anfänger würde nicht bestreiten wollen, dass ein geschriebener ctor eine Funktion ist. Aber selbst ein impliziter ctor oder einer mit nur einer Initialisierungsliste braucht Laufzeit. Auch wenn das lächerlich wenig ist, meinst du der Speicher füllt sich von allein?

    SeppJ schrieb:

    Die Exception und alles andere kannst du dir gleich sparen, indem du vernünftig programmierst und nicht irgendwelche Zeiger löscht,

    Ich lösche nicht irgendwelche Zeiger. Das löschen ist auch wie schon mal angemerkt völlig uninteressant. Viel wichtiger ist es keine falschen Zeiger zu dereferenzieren. Und dass kann ich mit NULL auch viel besser machen, als wenn ich mir das Programm um die Ohren hauen lasse.

    SeppJ schrieb:

    von denen du nicht einmal weißt, wohin sie zeigen.

    Was ja nur der Fall wäre, wenn ich sie nicht nullen würde. Siehe das Beispiel, wo der Zeiger oder besser die Adresse, schon wieder für eine Allokation verwendet wurde.

    SeppJ schrieb:

    Wenn du ein delete zu viel machst, dann hat dein Programm einen Fehler. An anderer Stelle. Und du merkst es noch nicht einmal, wenn du immer blind alles nullst.

    Ich nulle nicht blind alles, sondern nur Zeiger die ich gelöscht habe. Somit merke ich dann in dem Fall schmerzlos, dass ich das Objekt zu früh gelöscht habe. Wenn ich einfach einen gültigen Zeiger NULL setze und damit die Verbindung verliere, um das Objekt zu löschen, dies wäre fatal, richtig.


  • Mod

    Paul Müller schrieb:

    SeppJ schrieb:

    Du willst mir nicht ernsthaft erzählen, dass da tatsächlich eine Funktion aufgerufen wird.

    Ich hoffe das soll ein Scherz sein. Selbst ein Anfänger würde nicht bestreiten wollen, dass ein geschriebener ctor eine Funktion ist. Aber selbst ein impliziter ctor oder einer mit nur einer Initialisierungsliste braucht Laufzeit. Auch wenn das lächerlich wenig ist, meinst du der Speicher füllt sich von allein?

    Und der Anfänger läge damit falsch. Du auch. Seit wann ist Speicherfüllen ein Funktionsaufruf?



  • SeppJ schrieb:

    Paul Müller schrieb:

    SeppJ schrieb:

    Du willst mir nicht ernsthaft erzählen, dass da tatsächlich eine Funktion aufgerufen wird.

    Ich hoffe das soll ein Scherz sein. Selbst ein Anfänger würde nicht bestreiten wollen, dass ein geschriebener ctor eine Funktion ist. Aber selbst ein impliziter ctor oder einer mit nur einer Initialisierungsliste braucht Laufzeit. Auch wenn das lächerlich wenig ist, meinst du der Speicher füllt sich von allein?

    Und der Anfänger läge damit falsch. Du auch. Seit wann ist Speicherfüllen ein Funktionsaufruf?

    Also ihr sprecht da irgendwie über 2 verschiedene Sachen.

    Initialisierung von Variablen kostet Zeit gegenüber dem Nichtinitialisieren. Ich glaube da sind wir uns einig.

    Und ob der Konstruktor ein Funktionsaufruf ist oder nicht ist völlig plattformabhängig und wahrscheinlich auch davon was der Konstruktor im Endeffekt tut. Wenn es nur einen leeren Defaulkonstruktor gibt wird da wahrscheinlich nichts gemacht, sind aber mehrere verschiedene Konstruktoren vorhanden werden das sehr wahrscheinlich Funktionsaufrufe sein. Man müsste das halt mal überprüfen und ein wenig asm anschauen, aber ich kann mir das kaum anders vorstellen.



  • SeppJ hat vollkommen recht.

    Denn wenn man im Ctor 3h lang initialisiert, so muss man das ohne Ctor ja auch tun, sonst wäre der Code nicht äquivalent. Wenn ich 500 Variablen füllen muss, muss ich das ob ich Ctors habe oder nicht.

    Abstraktion kostet keine Performance in C++.



  • Ich glaube, es geht dem Paul um den Overhead für Funktionsaufrufe. Da sollte man wissen, dass sich Funktionsaufrufe inlinen lassen...


  • Mod

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


  • Mod

    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; und int_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.


Anmelden zum Antworten