small object allocator: Aufruf von Destruktor notwendig?



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



  • dd++ schrieb:

    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

    Ja, aber wenn

    for (...)
      data[i]::~type();
    delete[] pool;
    

    gleich

    for (...)
      /* nothing to do, it's trivial */;
    delete[] pool
    

    ist, macht der optimierende Compiler daraus

    /* nothing to destruct */
    delete[] pool;
    

    Du sparst dir also nichts.

    dd++ schrieb:

    von dem std::vector kann ich in diesem Anwendungfall aus mehreren Gründen nur abraten.

    Für solche Fälle gibt es auch std::deque .



  • Nö, wird nicht ganz wegoptimiert.

    Aus

    struct Foo
    {
        int bar;
    
        Foo() : bar(0) { }
        ~Foo() { bar = 1; }
    };
    
    Foo* mem = (Foo*)std::malloc(sizeof(Foo) * 1000);
    for(size_t i = 0; i < 1000; ++i)
        new(mem + i) Foo;
    
    ... Code ...
    
    for(size_t i = 0; i < 1000; ++i)
        mem[i].~Foo();
    std::free(mem);
    

    wird der Compiler allerhöchstens machen:

    Foo* mem = (Foo*)std::malloc(sizeof(Foo) * 1000);
    std::fill((int*)mem, (int*)mem + 1000, 0);
    
    ... Code ...
    
    std::fill((int*)mem, (int*)mem + 1000, 1);
    std::free(mem);
    

    Ich halte es für sehr unwarscheinlich dass der Compiler weiß dass nach dem Aufruf von std::free der Speicher nicht mehr genutzt wird und der Aufruf der Destruktoren unnötig ist.



  • Ethon schrieb:

    Nö, wird nicht ganz wegoptimiert.

    1. Mein GCC optimiert den vollständig weg. Alles was bleibt ist ein free().
    2. Es ging um trivially destructible Typen, den Destruktor wegzulassen ist hier UB.
    3. So ein Destruktor hat keinen Sinn.



  • jamenschenskind schrieb:

    2. Es ging um trivially destructible Typen, den Destruktor wegzulassen ist hier UB.

    geh noch mal in die Schule und lern lesen. vielleicht findest du dann auch heraus, wie man sich hier im Forum anmeldet

    im ersten Post geht es um eine Klasse, die von MFC CObject erbt. diese Klasse wiederum hat einen leeren virtuellen Destruktor. laut 12.4 Destructors 5 ist das ein nicht-trivialer Destruktor. dieser Destruktor hat aber offensichtlich keinen Effekt oder anders ausgedrückt keinen Einfluß auf den weiteren Programmablauf. laut 3.8 Object lifetime 4 kann man den Speicher dieses Objekts freigeben, ohne den Destruktor aufzurufen



  • Das Problem sollte nun gelöst sein. Memory pool war das Stichwort. Ich verwende nun boost::object_pool statt Loki::SmallObjAllocator. Die Destruktoren werden jetzt aufgerufen (beim Zerstören des pools).

    Das das nicht Aufrufen des Destruktors unter bestimmten Umständen "legal" ist, ist auf jeden Fall gut zu wissen.

    Danke an alle für die Hilfe 🙂



  • einspruch schrieb:

    dd++ schrieb:

    von dem std::vector kann ich in diesem Anwendungfall aus mehreren Gründen nur abraten.

    Für solche Fälle gibt es auch std::deque .

    std::deque wäre cool, wenn man die Page-Size einstellen könnte. Die ist nämlich typischerweise viel zu klein wenn es darum geht auf Geschwindigkeit zu optimieren.
    std::deque ist halt wirklich darauf optimiert eine double ended queue zu sein, und nicht ein Pool/Small-Object-Allocator.


Anmelden zum Antworten