small object allocator: Aufruf von Destruktor notwendig?



  • In einer Anwendung habe ich viele (mehrere Millionen) "einfache", kleine Objekte. Speicher wird über SmallObjAllocator aus der Loki-Lib alloziert und freigegeben.

    Das Funktioniert auch gut.

    Problem: das freigeben über Loki::SmallObjAllocator::Deallocate(memoryPtr) dauert sehr lange (mehrere Minuten).

    Vermutung: die Objekte müssen zuerst gefunden werden damit ihr Speicher freigegeben wird. D.h. jedes Loki::SmallObjAllocator::Deallocate(memoryPtr) sucht nach dem Speicher-Block memoryPtr.

    Rufe ich die Destruktoren der einzelnen Objekte nicht auf und lösche nur den small object allocator geht das löschen sehr schnell (<1s, nicht weiter gemessen). Der Speicher wird auch anscheinend freigegebe.

    Dazu 2 Fragen:

    1)
    Müssen die Objekte über delete gelöschen (muss ihr Destruktor aufgerufen werden)? Oder reicht es, wenn nur der small object allocator zerstört wird (und damit der zuvor angeforderte Speicher freigegeben wird)? Es werden keine Desruktoren der kleinen Objekte aufgerufen.

    Die kleinen Objekte müssen keine weiteren Resourcen freigeben (kein Speicher mit new alloziert o.ä.).

    Die Objekte sehen etwa so aus:

    class Line // diese Ojekte werden mit new angelegt, Speicher über small object allocator
    {
      private:
        Point start;
        Point end;
    };
    
    class Point
    {
      private:
         double x;
         double y;
    };
    

    2)
    Kleiner Zusatz:
    Die Objekte sind (leider) von CObject (MFC) abgeleitet. Könnte das zusätzliche Probleme bringen?



  • Wieso speicherst du deine mehrere Millionen Objekte nicht in einem std::vector?



  • Das habe ich noch nicht probiert. Wobei ich hier etwas skeptisch bin, ob das effizienter ist.

    Mein Frage bleibt: reicht es bei solchen Objekten den Speicher freizugeben ohne die Destruktoren aufzurufen? Ist das "legal" - d.h. entsteht kein undefiniertes Verhalten und ist der Speicher dann auch korrekt/restlos freigegeben?



  • ihoernchen schrieb:

    Das habe ich noch nicht probiert. Wobei ich hier etwas skeptisch bin, ob das effizienter ist.

    Wette niemals gegen std::vector, die Wette verlierst du meinst :).

    ihoernchen schrieb:

    Mein Frage bleibt: reicht es bei solchen Objekten den Speicher freizugeben ohne die Destruktoren aufzurufen? Ist das "legal" - d.h. entsteht kein undefiniertes Verhalten und ist der Speicher dann auch korrekt/restlos freigegeben?

    Ich habe noch nie mit Loki ode MFC gearbeitet, kann dir daher keine Antwort geben. Ich verwende generell Dinge aus der Standardbibliothek.



  • ihoernchen schrieb:

    Das habe ich noch nicht probiert. Wobei ich hier etwas skeptisch bin, ob das effizienter ist.

    Mein Frage bleibt: reicht es bei solchen Objekten den Speicher freizugeben ohne die Destruktoren aufzurufen? Ist das "legal" - d.h. entsteht kein undefiniertes Verhalten und ist der Speicher dann auch korrekt/restlos freigegeben?

    Du kannst Objekte ohne benutzerdefinierten Destruktor, die weder eine Basisklasse mit benutzerdefiniertem Konstruktor noch Datenmember mit benutzerdefiniertem Destruktor besitzen, auf die lautlose Weise um die Ecke bringen und einfacch ihren Speicher abschlachten, jo.

    std::is_trivially_destructible ist dein Freund.



  • Ethon schrieb:

    Du kannst Objekte ohne benutzerdefinierten Destruktor, die weder eine Basisklasse mit benutzerdefiniertem **[De]**struktor noch Datenmember mit benutzerdefiniertem Destruktor besitzen, auf die lautlose Weise um die Ecke bringen und einfacch ihren Speicher abschlachten, jo.

    Das ist nicht hinreichend.



  • Na, dann sag doch mal an was einen Destruktoraufruf ohne Effekt von einem nicht erfolgten Destruktoraufruf unterscheidet. 😉



  • Ethon schrieb:

    Na, dann sag doch mal an was einen Destruktoraufruf ohne Effekt von einem nicht erfolgten Destruktoraufruf unterscheidet. 😉

    Deine Definition war nicht rekursiv und lässt das hier durchgehen:

    struct KlasseOhnebenutzerdefiniertenDestruktor : BasisklasseOhnebenutzerdefiniertenDestruktor{};
    struct BasisklasseOhnebenutzerdefiniertenDestruktor {
      std::string member_mit_benutzerdefiniertem_destruktor;
    };
    

    Er nicht virtuell sein.

    struct KlasseMitVirtuellemDefaultDestruktor {
      virtual ~KlasseMitVirtuellemDefaultDestruktor() =default;
    };
    

    Ausserdem, ein "Destruktoraufruf ohne Effekt" ist nicht trivial, wenn der Destruktor als "{}" definiert ist.



  • Vielleicht hat jemand von diesem Forum Lust, das zu fixen:

    <a href= schrieb:

    http://en.cppreference.com/w/cpp/language/destructor">Trivial destructor
    The implicitly-declared destructor for class T is trivial if all of the following is true:
    The destructor is not virtual (that is, the base class destructor is not virtual)
    All direct base classes have virtual destructors
    All non-static data members of class type (or array of class type) have virtual destructors



  • ich korrigiere:

    12.4 Destructors
    5 ...
    A destructor is trivial if it is not user-provided and if:
    — the destructor is not virtual,
    — all of the direct base classes of its class have trivial destructors, and
    — for all of the non-static data members of its class that are of class type (or array thereof), each such class has a trivial destructor.
    Otherwise, the destructor is non-trivial.

    in der Praxis kann man bei vielen Objekten, die einen nicht-trivialen Destruktor haben, den Speicher frei geben, ohne den Destruktor aufzurufen. das läuft dann streng genommen als UB. wenn der Destruktor aber offensichtlich keinen Einfluß auf den weiteren Programmablauf hat und man damit meßbar Geschwindigkeit gewinnt, sollte man das auch so machen. für die Nachwelt sicherheitshalber noch einen Kommentar einfügen

    class MyInt
    {
    public:
      MyInt () { iValue = 0; }
      ~MyInt () { iValue = 1; }
    private:
      int iValue;
    };
    


  • dd++ schrieb:

    in der Praxis kann man bei vielen Objekten, die einen nicht-trivialen Destruktor haben, den Speicher frei geben, ohne den Destruktor aufzurufen. das läuft dann streng genommen als UB. wenn der Destruktor aber offensichtlich keinen Einfluß auf den weiteren Programmablauf hat und man damit meßbar Geschwindigkeit gewinnt, sollte man das auch so machen. für die Nachwelt sicherheitshalber noch einen Kommentar einfügen

    Nein 👎

    Es macht keinen Sinn, einen Destruktor ohne Einfluss zu deklarieren, wie in deinem Beispiel.

    Wenn der Destruktor Einfluss hat, muss er auch zwingend aufgerufen werden.

    Dass man "meßbar Geschwindigkeit gewinnt" liegt vermutlich daran, dass ihoernchen mit -O0 oder so kompiliert hat und die Zeit im Debug-Modus gemessen hat. Wenn der Destruktor keinen Einfluss hat, sollte er im optimierten Programm aufgerufen werden.



  • den Begriff "sinnvollen Destruktor" habe ich auf die Schnelle im ISO/IEC 2011 nicht gefunden. vielleicht sagst du uns noch schnell die Seite, wo wir das nachlesen können

    in dem Beispiel, das ich angegeben hatte, ist eine Klasse, die einen gemäß ISO/IEC 2011 nicht-trivialen Destruktor hat. dennoch kann man den Speicher eines derartigen Objekts freigeben, ohne den Destruktor aufzurufen, und es hat keinen Einfluß auf den weiteren Programmablauf

    der Geschwindigkeitsvorteil liegt darin, daß man nicht 1 Mio. mal 4 Bytes freigibt, sondern 4 Mio. Bytes in einem Rutsch



  • einspruch schrieb:

    Es macht keinen Sinn, einen Destruktor ohne Einfluss zu deklarieren, wie in deinem Beispiel.

    Doch, es kann Sinn machen. Siehe http://herbsutter.com/gotw/_100/



  • dd++: Der Speicher kann doch immernoch am Stück freigegeben werden⁉



  • Kellerautomat schrieb:

    einspruch schrieb:

    Es macht keinen Sinn, einen Destruktor ohne Einfluss zu deklarieren, wie in deinem Beispiel.

    Doch, es kann Sinn machen. Siehe http://herbsutter.com/gotw/_100/

    // header
    struct a {
     ~a();
    };
    // implementation
    a::~a() =default;
    


  • dd++ schrieb:

    der Geschwindigkeitsvorteil liegt darin, daß man nicht 1 Mio. mal 4 Bytes freigibt, sondern 4 Mio. Bytes in einem Rutsch

    Irgendwo müssen ja Speicherbereiche deiner Objekte, die allokiert wurden auch verwaltet werden...
    Wenn deine Speicherverwaltung dann noch 1 Mio Einträge hat, die nicht mehr existieren, dann ist es ja wohl auch ein Memory-Leak in der Verwaltung selbst.

    EDIT: Über die intern verwendete Speicherverwaltung zu spekulieren, wäre wohl auch etwas sehr riskant, es sei denn Loki - kenne ich persönlich nicht - macht dazu irgendwelche Angaben

    Generell schliesse ich mich der std::vector-Empfehlung an.
    Dieser hat (glaube ich i. d. R.) einen indirekten Speicherzugriff pro Index-Zugriff mehr als zwangsläufig notwendig. Wenn man allerdings darüber iteriert nur noch bei der Adressierung des ersten Elements.
    Wenn du unterschiedliche Indexzugriffe an einer Position deines Programms benötigst, kannst du dir den Zeiger auf die Daten auch einmal holen und dann per Array-Index darauf zugreifen:
    http://www.cplusplus.com/reference/vector/vector/data/
    Aber dann programmierst du schon eher C als C++ 😉

    Also intern verwendet std::vector (auch wenn der Standard da sicherlich keine Vorgabe macht) vermutlich Placement New: http://login2win.blogspot.de/2008/05/c-placement-new.html
    Wenn dein Compiler nun merkt, dass der Destruktor-Aufruf einem Aufruf einer leeren Funktion gleicht, wird er alle Destruktor-Aufrufe (zumindest bei entsprechenden Optimierungsflags) rausoptimieren.

    Falls du dich gegen std::vector entscheidest, würde ich mir Placement New anschauen, da du dann explizit sagst, dass du dich um die Speicherverwaltung kümmerst und damit Standard-konform bleibst. Ich bin mir jetzt nicht sicher, ob es Konstellationen gibt, wo man wirklich darüber etwas gewinnen kann. Aber ich vermute, du wirst so auch nicht (nennenswert) etwas gewinnen können.

    So ist das "Lösche alle Objekte gleichzeitig" von dd++ auch garantiert gültiger Code. Es spricht meiner Meinung nach nichts dagegen, dass der Compiler leere Destruktor-Aufrufe wegoptimieren kann, selbst wenn sie virtual sind, da du ja den Destruktor eines konkreten Objekttyps explizit aufrufst und somit die Adressierung über die vtable wegfällt. (Ich weiß, der Standard macht keine Aussage wie virtuelle Funktionen zu realisieren sind)

    Gruß,
    XSpille



  • Danke für die Antworten und Ideen 🙂

    Das Problem ist nicht der Destruktor Aufruf an sich. Das Problem ist, dass bei jedem Destruktor Aufruf der Speicherbereich gesucht wird und evtl. reorganisiert wird. Bei wenigen Objekten (<1 Millionen) ist noch alles ok.

    Sofern ich das zeitlich noch schaffe versuche ich die std::vector Lösung testweise zu implementieren. Ich habe nur meine Zweifel, ob das funktioniert mehrere hundert MB in einem Speicherblock zu haben - und ob das effizient ist. Meine std::vector Implementierung scheint den Speicher bei jeder Vergösserung zu verdoppeln. Es ist nicht wirklich effizient möglich die Anzahl der Objekte vorher genau zu bestimmen (somit ist ein sinnvolles reserve(x) nicht möglich).



  • ich komme noch mal zurück auf die ursprüngliche Frage

    Müssen die Objekte über delete gelöschen (muss ihr Destruktor aufgerufen werden)? Oder reicht es, wenn nur der small object allocator zerstört wird (und damit der zuvor angeforderte Speicher freigegeben wird)? Es werden keine Desruktoren der kleinen Objekte aufgerufen.

    im ISO/IEC 2011 habe ich dazu folgendes gefunden:

    3.8 Object lifetime
    1 ...
    The lifetime of an object of type T ends when:
    — if T is a class type with a non-trivial destructor (12.4), the destructor call starts, or
    — the storage which the object occupies is reused or released.
    4 A program may end the lifetime of any object by reusing the storage which the object occupies or by explicitly calling the destructor for an object of a class type with a non-trivial destructor. For an object of a class type with a non-trivial destructor, the program is not required to call the destructor explicitly before the storage which the object occupies is reused or released; however, if there is no explicit call to the destructor or if a delete-expression (5.3.5) is not used to release the storage, the destructor shall not be implicitly called and any program that depends on the side effects produced by the destructor has undefined behavior.

    d.h. bei einer Klasse, deren Destrukor keine Auswirkung (side effects) hat, kann man den Speicher feigeben, ohne den Destrukor aufzurufen. konkret hat auch MFC ~CObject() keine Auswirkung, siehe atlmfc\include\afx.h und atlmfc\include\afx.inl

    von dem std::vector kann ich in diesem Anwendungfall aus mehreren Gründen nur abraten. man sollte hier eine Lösung finden, bei der die Objekte in mehreren Pages gespeichert werden und nicht in einem großen zusammenhängenden Speicherblock

    wenn man z.B. ein 32-Bit Programm hat mit 2 GB verfügbarem Speicher, und davon ist noch 1 GB frei, dann kann es sein (und tritt auch häufig auf), daß man keinen zusammenhängenden Speicher von z.B. 100 MB allokieren kann. die freien 1 GB Speicher bestehen aus vielen kleinen Fragmenten, wobei da größte kleiner als 100 MB ist. in C++ gibt es keine Garbage Colllection, und demzufolge ist der einzige Ausweg aus dieser Situation, daß man es vermeidet, so große zusammenhängende Speicherblöcke zu verwenden



  • dann hol Dir doch am Anfang schon mal Speicher fuer den vector, sagen wir mal fuer 5 Mio Deiner Objekte (wenn das moeglich ist). Dann kommt es nur noch sehr selten zum reallozieren.

    Und da std::vector in Deiner Implementierung bei nicht ausreichendem Speicher direkt die doppelte Menge alloziert halbiert sich somit die Haeufigkeit der reallozierungen bei sonst gleich bleibender Algorithmik quasi wie von selbst.



  • ihoernchen schrieb:

    Das Problem ist nicht der Destruktor Aufruf an sich. Das Problem ist, dass bei jedem Destruktor Aufruf der Speicherbereich gesucht wird und evtl. reorganisiert wird. Bei wenigen Objekten (<1 Millionen) ist noch alles ok.

    Jo. CObject hat doch bestimmt einen virtuellen Destruktor. Also wird von jedem zu zerstörenden Objekt der vptr auf die vtbl angefaßt.
    Was Du brauchen tun tust, ist ein Memory Pool, kein Small Object Allocator.


Anmelden zum Antworten