@kingruedi



  • kingruedi schrieb:

    C++ federt das Problem eben mittels RAII ab. In Java und .NET ist das nicht möglich.

    Inwiefern glaubst du, dass es nicht möglich ist?

    Ich finde so was:

    try
    {
        ...
    }
    // Man beachte, dass ich keine Exception fange, will ich nämlich grad nicht.
    finally
    {
        obj.dispose();
    }
    

    schon ziemlich weich. Was ich an C++ schöner finde, ist dass der Aufruf eines Destruktors implizit ist und nicht von Hand hingeschrieben werden muss. In C++ braucht man das aber auch sehr viel eher, weil man selbst Speicher so verwalten muss. In Java hat man kaum finally, weil meistens geht es um Speicher, dne der GC schon macht. Für ein Socket macht man dann so einen Block.

    Wie skaliert eigentlich der Java und der .NET GC?

    Was genau meinst du? Der GC achtet darauf, dass er möglichst spät läuft. Je später er läuft, desto weniger arbeit hat er, weil er nur lebende kopiert. Damit er nicht zu viele auf einmal kopiert (was stocken würde), macht er öfter kleine Schritte und betrachtet dabei aber nicht den ganzen Heap (Generationen).

    Warum hat man eigentlich nicht komplett auf finalize verzichtet?

    Für ein Socket "braucht" man es ja. Mir stellt sich die Frage, warum man auf C++ Destruktoren verzichtet hat. Vielleicht hat man das selten in Java und hat sich dann gedacht, es ist besser, das wenige dann explizit zu haben und nicht versteckt.

    Aber ich bin einfach ein großer Fan davon, wenn sich ein Programm selbst aufräumt. Egal ob der Programmierer eine Exception wirft, irgend wo eine Funktion verlässt oder der Code normal durchläuft.

    Sowieso. Ist aber nichit nur in C++ möglich. 🙂



  • Inwiefern glaubst du, dass es nicht möglich ist?

    Ich weiß, dass man das so macht. Aber das finde ich eben ist einfach "Exception rumgewurschtel", bzw. Verwässerung. Man bastelst sich noch eine explizite Aufräumfunktion.

    Sowieso. Ist aber nichit nur in C++ möglich. 🙂

    In Java ist es aber nicht implizit.

    Meine Bedenken sind eben folgende: Ein Objekt stellt eine Abstraktion dar, es interagiert über eine Schnitstelle und versteckt seine Implementierung. Also sollte es auch darüber bestimmen können, wie es sich aufräumt. Wenn ich nun vom Programmierer explizit erwarten muss, dass er das Objekt aufräumt, geht ein Teil der Abstraktion flöhten. Naja, die Sicht eines Java-Programmierers ist wahrscheinlich, dass Aufäumen für ein Objekt nicht selbstverständlich ist und eben ein Teil der Schnitstelle und keine Grundlage eines Objekts ist.

    Was genau meinst du? Der GC achtet darauf, dass er möglichst spät läuft. Je später er läuft, desto weniger arbeit hat er, weil er nur lebende kopiert. Damit er nicht zu viele auf einmal kopiert (was stocken würde), macht er öfter kleine Schritte und betrachtet dabei aber nicht den ganzen Heap (Generationen).

    Ich wollte wissen wie er zur Anzahl der Objekte skaliert, also O-Notation. (Natürlich für einen kompletten Durchlauf).



  • kingruedi: Dann hältst du nicht mehr viel von Lisp? Da gibts ja auch nur unwind-protect als Gegenstück zu try..finally.



  • kingruedi schrieb:

    In Java ist es aber nicht implizit.

    Meine Bedenken sind eben folgende: Ein Objekt stellt eine Abstraktion dar, es interagiert über eine Schnitstelle und versteckt seine Implementierung. Also sollte es auch darüber bestimmen können, wie es sich aufräumt. Wenn ich nun vom Programmierer explizit erwarten muss, dass er das Objekt aufräumt, geht ein Teil der Abstraktion flöhten. Naja, die Sicht eines Java-Programmierers ist wahrscheinlich, dass Aufäumen für ein Objekt nicht selbstverständlich ist und eben ein Teil der Schnitstelle und keine Grundlage eines Objekts ist.

    Meine Sicht der Dinge ist, dass ein Objekt nicht von selber weiß, wann es sich aufräumen soll, aber wie. So ist es auch in Java. In C++ kann man diesen Hinweis implizit machen, was geil ist.
    Aber irgendwie geht das halt nicht immer. Wenn man ein Strategiespiel hat, dann weiß ich nicht, wann ich ne Einheit aufräumen kann. Wenn sie gestorben ist? Aber vielleicht wird sie noch von einer anderen Einheit anvisiert (oder einem Geschoss). Wenn sie verfault ist? Wer garantiert, dass sie nirgendwo mehr referenziert ist? Sehr gefährlich, sie aufzuräumen. In Warcraft III wird sie _glaube ich_ erst bei Spielende aufgeräumt. Es gibt funmaps, wo während des ganzen Spielverlaufs mehr als 10.000 Einheiten beteiligt sind und der RAM-Verbrauch steigt um viele MB an mit zunehmender Spieldauer. Ist das Spiel beendet, sinkt es wieder auf ein normales Niveau. Vielleicht ist das in einem komplexen Programm wirklich die einzige Möglichkeit, dafür verbraucht das Programm dann aber zeitweise mehr Speicher als es wirklich braucht.

    Und für sowas funktioniert RAII nicht mehr so gut IMHO. Du kannst schon sicherstellen, dass alles freigegeben wird, aber du musst es aufschieben, wenn es komplex wird. Ein GC macht sowas mit links. Und wenn die Regeln nicht immer so einfach sind, kann ein Objekt nicht wissen, wann es sich destruieren soll. Aber das ist jetzt nur meine Sicht der Dinge.

    Du kannst es vielleicht so sehen, dass finally-Blöcke in Java weit seltener sind als deterministische Destruktoren in C++. Die größten Sorgen nimmt der GC halt ab, für C++ wäre das System mit finally so indiskutabel, völlig klar.

    Ich wollte wissen wie er zur Anzahl der Objekte skaliert, also O-Notation. (Natürlich für einen kompletten Durchlauf).

    Das hängt sehr von der Anzahl der überlebenden Objekte ab. Die meisten sind kurzlebig, deshalb muss er vielleicht bei 2000 Objekten nur 10 kopieren.
    Überlebende Objekte kommen in ne höhere Generation und werden dann seltener betrachtet. Ich glaube nicht, dass man eine O-Notation aufschreiben kann, weil Sun und Microsoft schon seit Jahren daran tüfteln, welche Generationen-Größen ideal sind. Kleine Gen0 heißt häufig GC, heißt, dass Objekte kopiert werden, die kurz später gestorben wären. Große Gen0 heißt u.U. viele Objekte auf einmal kopieren, was man für Server macht. Die machen insgesamt sauwenig Arbeit, aber der GC braucht dann nicht ne zehntel ms sondern ne Sekunde oder mehr, wenn er mal was tut.
    Für Client VMs tuned sich der GC je nach Anwendungsprofil selbst und versucht dann, irgendwie eine halbe ms oder sowas als Maximum einzuhalten.

    Gregor hat mal jemanden im Forum reingelegt, der meinte, ein GC verschlechtere die Performance:

    for( int i = 0;  i < 1000000;  ++i )
        new Foo();
    

    Ist natürlich gemein, weil der GC hier überhaupt nichts macht. 😃 Kann man also eigentlich nur in Abhängigkeit von Generationengröße und Lebensdauer der Objekte angeben.



  • Bashar schrieb:

    kingruedi: Dann hältst du nicht mehr viel von Lisp? Da gibts ja auch nur unwind-protect als Gegenstück zu try..finally.

    so wie ich das jetzt verstanden habe, ist aber nicht sichergestellt wann der finally-block überhaupt genau aufgerufen wird.
    Oder irre ich mich?

    unwind-protect wäre da dann konsequenter.



  • Er wird genau dann aufgerufen, wenn der zugehörige try-Block verlassen wird, auf welchem Wege auch immer.



  • also eigentlich doch genau wie der c++ destruktor eines objekts auf dem stack?

    das ist dann doch wunderbar. Sogar besser als in C++ wo man für sowas erst eine Klasse erstellen muss.



  • DrGreenthumb: Das nicht-deterministische gilt für finalize(). Das wird erst aufgerufen, wenn der GC das Objekt abräumt. Was ganz anderes als finally.



  • ach, und da soll einer durchsteigen 😉
    also schließe ich daraus für mich, finalize blöd, finally toll



  • DrGreenthumb schrieb:

    also eigentlich doch genau wie der c++ destruktor eines objekts auf dem stack?

    das ist dann doch wunderbar. Sogar besser als in C++ wo man für sowas erst eine Klasse erstellen muss.

    nein, das nehme ich zurück. Schließe mich dem an, dass das Objekt für seine Löschung verantworlich sein sollte, ohne das der Aufrufer extra cleanup() aufrufen muss.
    Das unwind-protect ist eigentlich auch nur so praktisch, weil man's zB. in ein with-macro wrappen kann.



  • Aber irgendwie geht das halt nicht immer. Wenn man ein Strategiespiel hat, dann weiß ich nicht, wann ich ne Einheit aufräumen kann. Wenn sie gestorben ist? Aber vielleicht wird sie noch von einer anderen Einheit anvisiert (oder einem Geschoss). Wenn sie verfault ist? Wer garantiert, dass sie nirgendwo mehr referenziert ist? Sehr gefährlich, sie aufzuräumen.

    Das Problem versteh ich nicht. Wenn die Einheit ein RefCounted Objekt ist, kann sie jeder wegschmeißen, wenn er sie nicht mehr braucht. Wenn sie der letzte wegschmeißt, wird sie freigegeben. Ist doch ok? Wo ist das RefCounting unzuverlässig?

    Das Problem das ich hier sehe ist eher ein semantisches. Ist eine zerstörte verfaulende Einheit noch eine Einheit mit einem bestimmten Attribut oder sind das andere Objekte? Das kann man aber doch über Vererbung und another level of indirection lösen und hat nix mit aufräumen zu tun.

    Manchmal fänd ich ein finally schon auch in c++ ganz cool. Hin und wieder kommts vor, dass ich alleine zum Zweck des exception-sicheren aufräumens eine in die Funktion eingebettete Klasse schreiben muss. Im Dtor steht dann das, was im finally-Block stehen würde. Nur muss ich viel mehr tippen: Klassendeklaration, Ctor, dem referenzen auf die zusetzenden Objekte gegeben werden etc.



  • kartoffelsack schrieb:

    Manchmal fänd ich ein finally schon auch in c++ ganz cool. Hin und wieder kommts vor, dass ich alleine zum Zweck des exception-sicheren aufräumens eine in die Funktion eingebettete Klasse schreiben muss. Im Dtor steht dann das, was im finally-Block stehen würde. Nur muss ich viel mehr tippen: Klassendeklaration, Ctor, dem referenzen auf die zusetzenden Objekte gegeben werden etc.

    Tust du dir einen ScopeGuard schreiben oder den von Alexandrescu nehmen.



  • ah ja: http://www.cuj.com/documents/s=8000/cujcexp1812alexandr/

    sowas hab ich mir auch schon selber gebaut. Nur hatte ich da immer Probleme mit konstanten Objekten, Referenzen ... und war weit hausbackener natürlich.
    Sieht gut aus. Vielen Dank!

    Nur, hm, wo kann ich das ganze denn fertig downloaden? Der Link, der hier angegeben ist http://zfx.p15139172.pureserver.info/DisplayThread.php?TID=15376 funzt nicht. Und in welcher Boost-Bibliothek kann ich das ganze finden?



  • kartoffelsack schrieb:

    Das Problem versteh ich nicht. Wenn die Einheit ein RefCounted Objekt ist, kann sie jeder wegschmeißen, wenn er sie nicht mehr braucht. Wenn sie der letzte wegschmeißt, wird sie freigegeben. Ist doch ok? Wo ist das RefCounting unzuverlässig?

    Bei zirkulären Abhängigkeiten, die keineswegs eine Seltenheit sind.

    Das Problem das ich hier sehe ist eher ein semantisches. Ist eine zerstörte verfaulende Einheit noch eine Einheit mit einem bestimmten Attribut oder sind das andere Objekte? Das kann man aber doch über Vererbung und another level of indirection lösen und hat nix mit aufräumen zu tun.

    Ist doch egal was es ist. Wenn sie irgendwo referenziert wird, kann ich sie nicht gefahrlos deleten, und zwar so lange nicht, bis auch der letzte ->isDead() gemacht hat und die Referenz aufgehoben hat. Wenn das Teil bereits deleted ist, kann ich isDead() jedenfalls nicht mehr fragen. Indirektionen sind lame und was hat Vererbung damit zu tun? Ne, lieber nichts deleten und beim Spielende alles kicken. Dann kann nichts passieren. Wenigstens ein Herzinfarkt-Risiko weniger.

    Manchmal fänd ich ein finally schon auch in c++ ganz cool.

    Naja, ich nicht. aber zumindest ein paar namhafte Sprachdesigner anscheinend schon, also Geschmackssache vielleicht.



  • Bashar schrieb:

    kingruedi: Dann hältst du nicht mehr viel von Lisp? Da gibts ja auch nur unwind-protect als Gegenstück zu try..finally.

    In Lisp bin ich bisher nicht auf das Problem gestoßen. Aber es ist ja nicht unbedingt ein K.O.-Kriterium für eine Programmiersprache. Es lässt sich bei einem Garbage Collector wohl nicht anders lösen. Mich stört das in Java wahrscheinlich einfach, weil ich Java zu sehr aus meiner C++-Sicht sehe. Bei Lisp komm ich mit der C++-Sicht eh nicht weiter 😉 (das ist ja schon bei Java kaum möglich ;))

    Optimizer schrieb:

    Bei zirkulären Abhängigkeiten, die keineswegs eine Seltenheit sind.

    Öhm, was genau sind zirkuläre Abhängigkeiten?

    Ist doch egal was es ist. Wenn sie irgendwo referenziert wird, kann ich sie nicht gefahrlos deleten, und zwar so lange nicht, bis auch der letzte ->isDead() gemacht hat und die Referenz aufgehoben hat.

    Bei deinem Beispiel kann man das Problem doch sehr gut mit Referenzcounting lösen.



  • kingruedi schrieb:

    Optimizer schrieb:

    Bei zirkulären Abhängigkeiten, die keineswegs eine Seltenheit sind.

    Öhm, was genau sind zirkuläre Abhängigkeiten?

    A hält eine Referenz auf B und B eine auf A; damit verhindern sie, daß sie jemals freigegeben werden können, wenn der RC kein "cyclic"-flag hat, o.ä.. Dann kann man aber auch gleich Mark&Sweep nehmen. RC bringt es eigentlich nur, wenn man richtig viele (oder große) Daten hat, sonst ist jeder halbwegs moderne GC sogar schneller(!).



  • kingruedi schrieb:

    Bei deinem Beispiel kann man das Problem doch sehr gut mit Referenzcounting lösen.

    Nein, wegen den zirkulären Abhängigkeiten leider nicht. Daniel E. hat die gegenseitige Abhängigkeit, ein Spezialfall, schon genannt. Gegen andere Fälle hilft AFAIK auch kein flag im Smartpointer.



  • Naja, kommt drauf an wie man modelliert.

    Ein Objekt, das einen Smart-Pointer auf einen anderen hat, ist ein Besitzer dieses Objekts.
    In Deinem Strategiespiel ist aber die Einheit A, die die Einheit B im Visier hat aber nicht der Besitzer von B. Sie kennt B. Deswegen hätte sie auch keinen Ref-Counted Pointer auf B sondern nen normalen (oder nen weak_ptr).
    B (und jedes andere Objekt) wäre eher an einer zentralen Stelle registriert. Diese würde über ein Observer-Pattern von B darüber informiert werden, wenn B zerstört wird. Analog könnte A direkt von B oder von der zentralen Instanz informiert werden (das ist die zusätzliche Indirektionsebene, die ich meinte).
    Hört sich für das banale Beispiel jetzt vielleicht kompliziert an, aber ich denke in einer realen Anwendung müsste man sowieso mit einer zentralen Registratur und div. Obervern arbeiten.
    D.h. ich bin mir nicht sicher, ob das Problem mit den zirkulären Abhängigkeiten nicht theoretisch oder ein Hinweis auf unsauberes Design ist. Mir persönlich ist es auch erst einmal in der Bib von nem Kollegen begegnet. Und da war es mE falsch eingesetzt.
    Aber ok, mit ner GB braucht man sich von der Seite erstmal keine Sorgen zu machen.

    PS.: Wie merkt eigentlich eine GC, dass es sich um zirkuläre Abhängigkeiten handelt.



  • kartoffelsack schrieb:

    ... (und jedes andere Objekt) wäre eher an einer zentralen Stelle registriert. Diese würde über ein Observer-Pattern von B darüber informiert werden, wenn B zerstört wird. Analog könnte A direkt von B oder von der zentralen Instanz informiert werden (das ist die zusätzliche Indirektionsebene, die ich meinte).

    Ja, könnte ne Lösung sein. Ist aber echt lame, weil ne Einheit während ihrer Lebenszeit ungefähr 200mal ein anderes Objekt anvisiert und sich jedesmal beim Observer an- und abmelden muss. Wenn es nicht um Gigabyteweise RAM geht, bevorzuge ich für so einen Fall die Lösung mit am Ende freigeben (Spielende != Programmende).
    Nicht dass wir uns misverstehen: Ich komm schon irgendwie mit der Speicherverwaltung klar. 😉 Es gibt halt nur Fälle wo ein GC einfach mehr rockt und das ist so einer. Effizienter als der Observer mit Millionen von Zustandsänderungen während eines Spiels wäre er mit hoher Wahrscheinlichkeit.

    PS.: Wie merkt eigentlich eine GC, dass es sich um zirkuläre Abhängigkeiten handelt.

    Der GC sucht erreichbare Objekte. Es kümmert ihn nicht, wenn zwei unerreichbare sich gegenseitig referenzieren, weil er zu beiden nie hin kommt. Er geht von ein paar roots aus und verfolgt alle Pointer um erreichbare Objekte zu bestimmen. Wurzeln sind u.a.:
    - Prozessorregister
    - Alle Stackframes aller Threads

    Er hat von allen Dingen (wie Stackframes) statische Metadaten, die sagen, welche Bytes Pointer sind und welche nicht. Es gibt auch GCs (welche für Standard C++), die das nicht haben. Die sucken aber eh, weil sie Objekte nicht verschieben können, die lass ich mal außen vor. Wenn ich GC sage, meine ich immer einen männermäßigen GC. 😉 🤡



  • Optimizer schrieb:

    Wenn ich GC sage, meine ich immer einen männermäßigen GC. 😉 🤡

    Pah, ECHTE Männer brauchen keinen GC 🤡 1.

    1~Der Autor behält sich vor trotzdem Sprachen zu verwenden die einen garbage-collector besitzen, obwohl es nicht männlich ist.~


Anmelden zum Antworten