GC vs RAII



  • Wo sind die Vor- und wo die Nachteile von dem GC in Java im Vergleich zu RAII und Smartpointern in C++?



  • GC:
    Du hast einen Extra-Thread, der während der Ausführung des Hauptprogrammes asynchron (wenn z.B. Speicher angefordert werden muss) startet, um alle reservierten Speicherbereiche zu prüfen. Wenn es unreferenzierte Speicherbereiche gibt, werden sie freigegeben. Das heißt, in der Theorie. In der Praxis kann das anders sein. Perl z.B. hat auch GC, gibt den Speicherbereich aber nicht zurück an das OS, sondern reserviert halt nur neu. Sprich: du forderst für 5 Sekunden 2 GB, danach brauchst du sie nicht mehr. Aber die 2 GB gibt Perl nicht an das OS zurück, solange der Prozess lebt, aber wenn du nach 20 Stunden wieder 2 GB anforderst, wirst du möglicherweise wieder die gleichen 2 GB bekommen. Ob das bei Java auch so ist, kann ich allerdings nicht sagen.

    Vorteile:
    - Du kannst programmieren wie Sau, Objekte allozieren, und dich nicht darum kümmern, das Zeug wieder freizugeben.
    Nachteile:
    - Langsam. Java ist generell langsam. Java-Fanboys werden gegen die Aussage natürlich Sturm laufen, aber ist halt so. Perl ebenfalls. Kann man kaum was gegen machen.

    RAII:
    Du hast Konstruktoren, die automatisch aufgerufen werden, wenn ein Objekt erstellt wird - das ist dann super, wenn du z.B. von C zu C++ wechselst und keine Init-Routine für deine Strukturen mehr benötigst, der Compiler fügt die Calls automatisch ein - und Destruktoren - auch super, wieder aus den gleichen Gründen, weil du keine free- oder destroy-Routine benötigst, die gibt es ja schon. Damit hast du in der Theorie weder Probleme mit uninitialisierten Objekten noch mit Speicherlecks, weil du wieder ein delete vergessen hast.

    Vorteile:
    - Solange du dich an gewisse Regeln hält, kannst du auch wieder programmieren wie Sau.
    - Kann auch langsam sein. Sagen wir, du willst erst mal nur 100.000 Objekte auf dem Heap haben, diese aber nicht unbedingt initialisieren. Mit RAII hast du dann wieder 100.000 Calls des Konstruktors, die gemacht werden müssen. Vielleicht benötigen die Objekte selbst noch Speicher, müssen wieder Objekte aufrufen. Dann muss new Kernel-Calls machen, um mehr Speicher vom Kernel anzufordern. Kernel-Calls bedeuten Kontextwechsel, und die sind langsam, vor allem auf RISC-Plattformen (x86/x64-x86 sind CISC, die sind nicht so teuer, möchtest du aber trotzdem vermeiden). Wenn du nur die Objekte im Speicher haben willst, aber nicht initialisiert, dann musst du wieder malloc/free verwenden, und hast damit wieder den Schutz von Konstruktor und Destruktor weggeschmissen.
    Typisches Beispiel: Ich habe einen statischen Thread-Pool, der so und so viele Threads unterstützt. Wenn mehr als n Threads laufen sollen, muss der letzte verweigert werden. Den Pool auf dem Heap möchtest du schon haben, aber die Objekte sollen erst dann initialisiert werden, wenn sie auch wirklich gebraucht werden.

    Wenn dem was fehlen sollte oder ich was falsch geschrieben habe, lasse ich mich gerne belehren. 🙂


  • Mod

    dachschaden schrieb:

    - Langsam. Java ist generell langsam. Java-Fanboys werden gegen die Aussage natürlich Sturm laufen, aber ist halt so. Perl ebenfalls. Kann man kaum was gegen machen.

    Wenn Javaprogramme im Vergleich zu C++ Programmen langsamer sind, dann liegt das IMHO fast nie am GC. So etwas liegt wesentlich wahrscheinlicher daran, dass in Java u.a. wegen einem kleinen Overhead fuer jedes Objekt deutlich mehr Speicher benoetigt wird. Wenn die Speicher-Bandbreite oder die Cache-Groesse zu einem Flaschenhals in dem Programm wird, dann wird man mit C++ leicht ein performanteres Programm zu Stande kriegen.

    Der GC ist im Allgemeinen nicht langsam. Wir hatten in diesem Forum vor langer Zeit mal einen Benchmark-Test dazu gemacht, in dem wir gesehen haben, dass man sich in C++ schon sehr anstrengen muss, um bei einer Nutzung des Heaps auf eine aehnliche Performance zu kommen. Die Nutzung des Stacks ist allerdings immer schneller, keine Frage. Man muss beim GC ja auch sehen, dass er praktisch nur dann Zeit benoetigt, wenn er aktiv wird. Das ist zum Beispiel dann der Fall, wenn der Speicher, den er ueberwacht, voll zu werden scheint.

    Es gibt beim GC auch noch ganz andere Aspekte. Der Standard-GC in Java ist zum Beispiel AFAIK ein generationenbasierter GC. Das sind GCs, die letztendlich zu einer geringeren Fragmentierung des Speichers fuehren. Ich denke, bei einer naiven Nutzung des Heaps in C++ hat man bezueglich Speicherfragmentierung ein groesseres Problem. ...beim Stack fragmentiert natuerlich gar nichts.

    Ein Nachteil ist sicherlich, dass es schwer ist, zu steuern, wann ein GC Zeit benoetigt. Um diesen Nachteil zu beheben, hat man typischerweise unterschiedliche GCs zur Verfuegung, so dass man auch einen nutzen kann, der zu einer geringen maximalen Latenz fuehrt.



  • RAII kümmert sich um alle Ressourcen. Auch offen Datenbankverbindungen und eben alles, was sofort wegmuss. Ohne daß man sich im Code drum kümmern muss.

    Der GC tut nur das Speicherproblem lösen, was mir viel zu wenig ist.

    Der GC ist theoretisch schneller.

    Manchmal isses gemein lästig, wenn Klassen aus der Standardlib keine tiefen Kopien können.



  • GCvsRAII schrieb:

    Sagen wir, du willst erst mal nur 100.000 Objekte auf dem Heap haben, diese aber nicht unbedingt initialisieren. Mit RAII hast du dann wieder 100.000 Calls des Konstruktors, die gemacht werden müssen. Vielleicht benötigen die Objekte selbst noch Speicher, müssen wieder Objekte aufrufen. Dann muss new Kernel-Calls machen, um mehr Speicher vom Kernel anzufordern. Kernel-Calls bedeuten Kontextwechsel, und die sind langsam, vor allem auf RISC-Plattformen (x86/x64-x86 sind CISC, die sind nicht so teuer, möchtest du aber trotzdem vermeiden). Wenn du nur die Objekte im Speicher haben willst, aber nicht initialisiert, dann musst du wieder malloc/free verwenden, und hast damit wieder den Schutz von Konstruktor und Destruktor weggeschmissen.
    Typisches Beispiel: Ich habe einen statischen Thread-Pool, der so und so viele Threads unterstützt. Wenn mehr als n Threads laufen sollen, muss der letzte verweigert werden. Den Pool auf dem Heap möchtest du schon haben, aber die Objekte sollen erst dann initialisiert werden, wenn sie auch wirklich gebraucht werden.

    Hilfe, nein!
    Wenn du 100.000 Objekte auf dem Heap haben willst, dann musst du sowohl in Java als auch in C++ 100.000 mal "new" machen und 100.000 mal den Ctor aufrufen. Da besteht kein Unterschied. An was du bei Java denkst wären 100.000 NULL Referenzen. Das ist ganz was anderes. Und auch das kannst du in C++ mit RAII haben, z.B. mit std::unique_ptr bzw. std::shared_ptr .

    Und wenn du die 100.000 Objekte nicht sofort brauchst, und nicht sofort initialisieren willst, dann nimmst du einfach std::vector::reserve. Nix malloc/free und nix weggeschmissen dadurch.

    Du verwechselst hier GC vs. RAII mit Reference- vs. Value-Semantics.

    Und natürlich bedeutet new auch nicht einen Kernel-Call pro new Aufruf.
    Auch hier, also was die Anzahl der Kernel-Calls angeht, besteht eigentlich kein Unterschied zwischen Java und C++.
    Die Runtimes beider Sprachen können sich aussuchen wie gross sie die Brocken machen die sie vom OS anfordern.

    Einen Vorteil hat Java natürlich schon, aber der bekommt es nicht dadurch dass irgend ein GC verwendet wird, sondern dadurch dass ein kompaktierender GC verwendet wird. Also einer der beim "Collecten" die noch Lebenden Objekte "zusammenschiebt", so dass dazwischen keine Löcher mehr bleiben.
    Dadurch kann die Allokationsfunktion extrem einfach werden, und durch die geringere Fragmentierung wird weniger RAM "verbraten". Was gut ist weil dadurch einerseits mehr RAM ans OS zurückgeben kann, und andrerseits bekommt man damit ne bessere Cache-Nutzung hin.



  • Kernel-Calls gibts da eh nicht, die Laugzeitumgebung holt sich gelegentlich sagen wie mal 64kB und gibt 32B-weise raus, das ist voll vernachlässigbar.

    Mal die Konstruktoren weggelassen.

    100.000 mal new in Java ist nicht mehr als 100.000 mal

    char* ret=ptrLast;
    ptrLast+=sizeof(Thingy)
    return ret;
    

    , während in C++ da jemand nach freiem Speicher suchen muss.

    Java: 1 Takt pro new
    C++: 30 Takte pro new

    Bein Aufräumen rächt sich es leider, C++ wieder 30 Takte und Java nicht abschätzbar, theoretisch schneller als 60. Nu hat man in C++ für

    Point p(2,3);
    

    auf dem Stack halt 0 zum Allokieren und 0 zum Deallokieren. Und man benutzt nur Stackvariablen. Kosten tun nur die komischen Klassen, die innendrin Freispeicher verwenden wie vector oder so. In Java kostet jede Klasse.

    Also der GC ist nicht schuld. Java braucht auch RAII, das ist mal klar.

    Und dann schauen wie mal, ob generational Umsortieren (um hot-Speicher in der Nähe zu halten und oft um Cache) die Kosten einbringt, die Zusatzdereferenzierungen kosten. Wir können es nicht wissen. Tun die Prozessorbauer einen doppelreferenzierungsbefehl superscalar rein? Oder sorgen sie allgemeiner für so viel schnellen Cache, daß alle hot-Bereiche normalerweise hot bleiben.

    Falls an obiger Darstellung irgendwas lächerlich klingt: Es war nicht meine Absicht. Ich hab Java total ernst genommen. Aber nu lese ich das und es ist eine Verspottung. Komisch.



  • hustbaer schrieb:

    Wenn du 100.000 Objekte auf dem Heap haben willst, dann musst du sowohl in Java als auch in C++ 100.000 mal "new" machen und 100.000 mal den Ctor aufrufen. Da besteht kein Unterschied. An was du bei Java denkst wären 100.000 NULL Referenzen. Das ist ganz was anderes. Und auch das kannst du in C++ mit RAII haben, z.B. mit std::unique_ptr bzw. std::shared_ptr.

    Stimmt. Habe ich vergessen zu schreiben. Lag wahrscheinlich daran, dass ich im Geiste davon ausgegangen bin, dass es bei Java dann sowieso egal ist von der Laufzeit her (warum eigentlich? Siehe unten). In beiden Sprachen machst du dann Konstruktorenaufrufe. Nur in C++ kannst du die mit malloc vermeiden. Oder geht das auch in Java?

    volkard schrieb:

    Kernel-Calls gibts da eh nicht, die Laugzeitumgebung holt sich gelegentlich sagen wie mal 64kB und gibt 32B-weise raus, das ist voll vernachlässigbar.

    hustbaer schrieb:

    Und natürlich bedeutet new auch nicht einen Kernel-Call pro new Aufruf.

    Mit solchen Aussagen wäre ich vorsichtig, das hängt, wie du bereits sagtest, von der Laufzeitumgebung ab. Ich erinnere mich, dass OS X bei jedem malloc und jedem free einen Kernel-Call ausgelöst hat.

    Kuckst du hier.

    Kommt halt immer darauf an, welches malloc und welches free du verwendest. Ordentliche Implementierungen holen sich direkt größere Chunks vom Kernel und verwalten reservierte Speicherbereiche in einer verketteten Liste. Ist auch langsamer als der Stack, aber immer noch besser als immer Kernel-Calls. Ich versuche immer, die Anzahl der Speicheranforderungen gering zu halten, seit ich mal gelesen habe, dass der Firefox beim einfachen Öffnen einer Seite und dann Schließen ein paar Millionen malloc und calloc macht.

    Meine Aussage war ja auch nicht, dass new immer Kernel-calls macht. 😃

    volkard schrieb:

    Also der GC ist nicht schuld. Java braucht auch RAII, das ist mal klar.

    Gregor schrieb:

    Es gibt beim GC auch noch ganz andere Aspekte. Der Standard-GC in Java ist zum Beispiel AFAIK ein generationenbasierter GC. Das sind GCs, die letztendlich zu einer geringeren Fragmentierung des Speichers fuehren. Ich denke, bei einer naiven Nutzung des Heaps in C++ hat man bezueglich Speicherfragmentierung ein groesseres Problem. ...beim Stack fragmentiert natuerlich gar nichts.

    Habe nicht geschrieben, dass Java wegen GC langsam ist, sondern dass GC und Java generell langsamer sind.
    In C oder C++ kannst du es natürlich ganz wild treiben und die Optimierungen, die die VM von Java durchführt, selbst machen. Hat den Vorteil, dass du lernst, bewusst mit Speicher umzugehen, und ist dann wieder schneller als Java und verbraucht nicht <dreistellige Zahl einfügen> Megabytes beim Start. Kannst du natürlich auch machen. Habe ich mal für einen Webcrawler in C verwendet. Das Ding lief zwei Tage und hatte dann 120 Sekunden reine Prozessorzeit von einem Kern. Weil ich versuche, den Speicher zu erhalten. Am Besten vor der eigentlichen Programmausführung. Und wenn es ganz wild kommt, habe ich hier in meiner Lib immer noch ein define für alloca .
    Meine Tests mit Java waren da nicht so zufriedenstellend. Aber gut, ich mache auch Code in C, Java ist da eher Sekundärsprache. Vielleicht habe ich da nur nicht die Vorteile von Java ausgenutzt (sollte die VM das nicht automatisch machen? Egal), und ich tue der Sprache da unrecht. Wie gesagt, ich lasse mich gerne korrigieren.

    Hm, jetzt, wo ich so darüber nachdenke, fällt mir noch was ein, mit dem man mallocs und frees sparen kann. Deswegen mag ich das Forum. Gutes Gedankenfutter hier. 🙂

    volkard schrieb:

    Und dann schauen wie mal, ob generational Umsortieren (um hot-Speicher in der Nähe zu halten und oft um Cache) die Kosten einbringt, die Zusatzdereferenzierungen kosten. Wir können es nicht wissen. Tun die Prozessorbauer einen doppelreferenzierungsbefehl superscalar rein? Oder sorgen sie allgemeiner für so viel schnellen Cache, daß alle hot-Bereiche normalerweise hot bleiben.

    Nein, ist keine Verspottung. Aber wer kann schon mit Superskalarität etwas anfangen, außer Compilerbauer und Nerds? 😉



  • dachschaden schrieb:

    Nein, ist keine Verspottung. Aber wer kann schon mit Superskalarität etwas anfangen, außer Compilerbauer und Nerds? 😉

    Alle hier.
    Wer Fragen hat, der fragt.
    Wer anderer Meinung ist, der widerspricht.
    Ich verstehe und liebe C++.de als Fachforum.



  • volkard schrieb:

    RAII kümmert sich um alle Ressourcen. Auch offen Datenbankverbindungen und eben alles, was sofort wegmuss. Ohne daß man sich im Code drum kümmern muss.

    Dafür gibt's in Java seit Version 7 das try-with-resources Statement.

    Manchmal isses gemein lästig, wenn Klassen aus der Standardlib keine tiefen Kopien können.

    Beziehst du das auf Java? Die clone()-Methode macht doch genau das. 😕

    L. G.,
    IBV



  • dachschaden schrieb:

    hustbaer schrieb:

    Und natürlich bedeutet new auch nicht einen Kernel-Call pro new Aufruf.

    Mit solchen Aussagen wäre ich vorsichtig, das hängt, wie du bereits sagtest, von der Laufzeitumgebung ab. Ich erinnere mich (*snip*)

    Ist jetzt Interpretationssache.
    Ich habe natürlich nicht gemeint dass es nicht möglich wäre dass 1x new == 1x Kernel-Call. Ich meinte bloss üblicherweise, und auf allen Plattformen die kein totaler Schrott sind, so ist.



  • IBV schrieb:

    volkard schrieb:

    RAII kümmert sich um alle Ressourcen. Auch offen Datenbankverbindungen und eben alles, was sofort wegmuss. Ohne daß man sich im Code drum kümmern muss.

    Dafür gibt's in Java seit Version 7 das try-with-resources Statement.

    Was auch sehr nötig war. Aber kein Ersatz für RAII wie man es in C++ machen kann ist.

    Konkret fehlt z.B. die Möglichkeit Member-Variablen als "owned resource" zu deklarieren. Die Idee dabei wäre dass einer Klasse die "owned resource" Member hat vom Compiler automatisch ein close() Funktion implementiert wird. Bzw. wenn man die "close" Funktion selbst implementiert (weil man noch andere Dinge machen darin muss), es eine Möglichkeit gibt darin dann eine "default-close" Funktion aufzurufen. Die dann wieder das macht was die compilergenerierte "close" Funktion gemacht hätte. Ich programmiere zwar kein Java, aber genau diese Funktion wünsche ich mir in C# schon öfter mal.

    Und dann wäre da noch die Sache mit den fehlenden Value-Types. Denn RAII heisst für mich genauso dass ich sowas wie shared_ptr implementieren kann, ohne dass der Client-Programmer wissen und dran denken muss wo er überall eine neue Kopie des shared_ptr erzeugen oder eine bestehende entsorgen muss. Und dafür braucht man Value-Types -- mit RAII Support. Auch das fehlt in C# leider *.

    IBV schrieb:

    Manchmal isses gemein lästig, wenn Klassen aus der Standardlib keine tiefen Kopien können.

    Beziehst du das auf Java? Die clone()-Methode macht doch genau das. 😕

    Ne ich denke er meint die Collections, die eben Collections und keine Container sind.

    * Ich weiss schon dass es Value-Types aka "structs" in C# gibt. Und dass auch "structs" IDisposable implementieren können. Und dass der Compiler für lokale Variablen von solchen Typen sogar selbständig den IDisposable.Dispose Aufruf einfügt. Nur wird das alles dadurch kaputt gemacht dass "structs" in C# immer "blittable" sind, d.h. einen compilergenerierten Copy-Ctor und Assignment-Operator haben der einfach nur Byte-für-Byte die Daten rumkopiert.



  • try-with-resources ist für mich überhaupt kein Ersatz für RAII.
    Das gab es im Prinzip schon immer, denn es macht ja nichts anderes als daraus ein try...catch...finally{dispose} zu basteln.
    Es verkürzt einfach nur die Schreibweise.
    Aber ich muss (müsste) weiterhin bei jeder Klasse die ich verwende nachschauen, ob die gerne was disposen möchte.
    In C++ nutze ich einfach die Klasse und wenn die was aufräumen möchte, muss ich davon nichts wissen.



  • dachschaden schrieb:

    - Kann auch langsam sein. Sagen wir, du willst erst mal nur 100.000 Objekte auf dem Heap haben, diese aber nicht unbedingt initialisieren. Mit RAII hast du dann wieder 100.000 Calls des Konstruktors, die gemacht werden müssen.

    Der Unterschied ist, dass man in Java alles auf dem Heap anlegt und in C++ eher nicht.

    Wenn ich 100.000 Objekte in Java benötige, dann muss ich 100.000 mal new aufrufen und dynamischen Speicher anfordern. Das geht in Java sehr schnell.

    Wenn ich 100.000 Objekte in C++ benötige, dann lege ich einen std::vector mit 100.000 Elementen an, welcher alle 100.000 Elemente mit einem new-Aufruf alloziiert. Und das geht mit ziemlicher Sicherheit schneller, als die sehr schnellen 100.000 Aufrufe in Java.

    Ich gehe mal davon aus, dass beide Sprachen der Optimierer keine Probleme hat, den Konstruktoraufruf geeignet zu optimieren. In C++ habe ich möglicherweise einen Standardkonstruktor, welcher Inline ist. Da "sieht" der Compiler, was er machen muss und hat die Möglichkeit zu optimieren. Bei Java sieht er es zur Laufzeit und auch dort werden ähnliche Optimierungen erfolgen.

    In C++ programmiere ich eben anders. Daher ist es nicht sinnvoll, die Allokation von Speicher zu benchmarken, da ich in C++ eben weniger Allokationen machen muss.

    So nebenbei ist in C++ der Code zum anlegen von 100000 Elementen auch wesentlich übersichtlicher:

    std::vector<Point> points(100000);
    
    List<Point> points = new ArrayList<Point>();
    for (int i = 0; i < 100000; ++i)
      points.add(new Point());
    


  • dachschaden schrieb:

    So nebenbei ist in C++ der Code zum anlegen von 100000 Elementen auch wesentlich übersichtlicher:

    std::vector<Point> points(100000);
    
    List<Point> points = new ArrayList<Point>();
    for (int i = 0; i < 100000; ++i)
      points.add(new Point());
    

    Das ist jetzt ein Scherz oder? Soviel Code für so eine simple Sache?

    Mir ist auch eigentlich bei Java egal, ob das theoretisch langsamer oder gleich schnell wie C++ ist. Ich sehe Java-Anwendungen und sehe C++-Anwendungen und da gewinnt bei mir IMMER das C++-Programm in Sachen Performance.

    Habt ihr euch mal den neuen SceneBuilder für JavaFX angesehene? Der hat letzlich beim Ausprobieren, von einer Hand voll Widgets, meinen dicken i7 mit guter Grafikkarte locker in die Knie gezwungen. Da ist Battlefield4 genügsamer.


  • Mod

    Wann habt Ihr das letzte mal eine Liste mit 100.000 "Standardpunkten" benoetigt?

    Zeigt doch mal den Unterschied, wenn Ihr 100.000 Punkte erstellt, die alle auf einer Funktion liegen. Also zum Beispiel, wenn Ihr etwas plotten moechtet. Besser noch: Baut mal eine Methode, aus der Ihr so eine Liste an Punkten rauskriegt und zeigt diese inklusive dem Methodenaufruf.



  • tntnet! Du unterschlägst aber, das die C++ Variante nicht der Java-Variante entspricht!
    In deinem C++ Beispiel kann man nur Objekte genau eines Typs anlegen: Point! Und zwar ausschließlich Points, keine Objekte die von Point erben.

    Jetzt kann man natürlich sagen, ich brauche nur den einen Typ. Aber dann sage das auch. So ist der Vergleich leider für den Popo! Weil sobald du Point als Basis-Typ haben willst, ist der C++ Vorteil wieder dahin!



  • Artchi schrieb:

    Weil sobald du Point als Basis-Typ haben willst, ist der C++ Vorteil wieder dahin!

    std::vector<std::shared_ptr<Point>> points(size, std::make_shared<Point>());
    

    Macht in etwa dasselbe wie der Javacode und ist zeilenmäßig trotzdem besser.



  • Na also, wenn dann bitte gleich richtig. Geht doch! Aber das von Tntnet war ne Frechheit!

    Um die Codekürze ging es mir nicht vorrangig. Das ist nur nice-to-have. Es geht darum, was es macht und dann müssen beide Beispiel gleiches können.


  • Mod

    Nathan schrieb:

    std::vector<std::shared_ptr<Point>> points(size, std::make_shared<Point>());
    

    Macht in etwa dasselbe wie der Javacode und ist zeilenmäßig trotzdem besser.

    Ist immer noch irrelevant. Niemand braucht 100.000 Punkt Objekte, die alle mit dem Standardkonstruktor erstellt wurden und somit alle gleich sind.

    Wenn Ihr zu einem realistischeren Szenario geht, dann wird die Codelänge nicht mehr wesentlich von einander abweichen.



  • Gregor! Ja, das stimmt. Ich verstehe das Ziel dieses künstlichen Konstrukts auch nicht. In der Praxis sieht Code anders aus. Selbst für einen Unittest wäre das Beispiel unrealistisch.


Anmelden zum Antworten