alignas bei placement new



  • Ich möchte C Code nach C++ portieren. Ich mache dies Schritt für Schritt da der C Code riesig ist und möchte nun einige in C entwickelten Datenstrukturen durch STL ersetzen.

    Ein Beispiel:

    typedef struct
    {
      Handle PolygonArrayHandle;
      Handle PointArrayHandle;
      Handle CommentHandle
    } MeasurementFolder;
    
    Handle CreateMeasurementFolder()
    {
       Hande h = Heap_Alloc(sizeof(MeasurementFolder));
       MeasurementFolder* F = Heap_Data(h);
      
       F->CommentHandle = 0;
       return Hande;
    }
    

    Handle bzw. Heap_Alloc() sind eine eigene geschriebene Speicherverwaltung in C und werden an allen Ecken und Kanten benutzt. Und ich möchte diese Schritt für Schritt entfernen.

    Ich möchte in MeasurementFolder PolygonArrayHandle und PointArrayHandle durch ein std::vector und CommentHandle durch ein std::string ersetzen, kann es aktuell aber nicht wg. dieser Art der Initialisierung.

    Deswegen überlege ich den Einsatz von placement new. Unter cppreference habe ich den folgenden Code gefunden:

    {
        alignas(T) unsigned char buf[sizeof(T)];
        // Statically allocate the storage with automatic storage duration
        // which is large enough for any object of type `T`.
        T* tptr = new(buf) T; // Construct a `T` object, placing it directly into your 
                              // pre-allocated storage at memory address `buf`.
        tptr->~T();           // You must **manually** call the object's destructor
                              // if its side effects is depended by the program.
    } 
    

    Wozu benötige ich hier das alignas(T)? Kann ich dieses evt. vernachlässigen?



  • @Quiche-Lorraine sagte in alignas bei placement new:

    Ich möchte in MeasurementFolder PolygonArrayHandle und PointArrayHandle durch ein std::vector und CommentHandle durch ein std::string ersetzen, kann es aktuell aber nicht wg. dieser Art der Initialisierung.

    Ich verstehe nicht was du meinst. Wegen welcher Art der Initialisierung?

    Deswegen überlege ich den Einsatz von placement new.

    Meinst du du willst das Heap_Alloc für MeasurementFolder beibehalten und MeasurementFolder dann mit placement new in dem per Heap_Alloc besorgten Speicher konstruieren?

    Unter cppreference habe ich den folgenden Code gefunden:

    {
        alignas(T) unsigned char buf[sizeof(T)];
        // Statically allocate the storage with automatic storage duration
        // which is large enough for any object of type `T`.
        T* tptr = new(buf) T; // Construct a `T` object, placing it directly into your 
                              // pre-allocated storage at memory address `buf`.
        tptr->~T();           // You must **manually** call the object's destructor
                              // if its side effects is depended by the program.
    } 
    

    Wozu benötige ich hier das alignas(T)? Kann ich dieses evt. vernachlässigen?

    Das alignas(T) ist nötig um sicherzustellen dass das Alignment von buf kompatibel mit T ist (also mindestens so gross wie alignof(T)). Denn wenn du ein T Objekt in einem Speicherbereich erzeugst der nicht passend aligned ist, dann hat dein Programm undefiniertes Verhalten.

    In der Praxis wird auf x86 meist nicht viel schlimmes passieren. Allerdings kann es dir keiner garantieren - für bestimmte Instruktionen ist auch auf x86 das Alignment wichtig. Und du solltest dich nicht drauf verlassen dass der Compiler keine solchen Instruktionen generiert.

    Die beste Lösung wäre wohl parallel zu Heap_Alloc noch ein Heap_Alloc_Aligned zu implementieren, dass du dann ala Heap_Alloc_Aligned(sizeof(T), alignof(T)) aufrufst. Heap_Alloc_Aligned müsste dann garantieren Speicher zurückzuliefern der passend aligned ist. Und natürlich musst du es so machen, dass Heap_Data noch mit den von Heap_Alloc_Aligned zurückgelieferten Handles funktioniert.

    Eine andere Möglichkeit wäre Heap_Alloc so anzupassen dass es immer Speicher liefert, der für alle "normalen" Typen passt - also für alle die nicht mittels alignas manuell "over-aligned" wurden. Für x86 sollte ein Alignment von 16 Byte ausreichen. Wobei die Funktion natürlich immer auf die grösste 2er Potenz zurückfallen kann, die nicht grösser als der angeforderte Speicherbereich ist. Also wenn 15 Byte angefordert werden muss das Alignment max. 8 sein, bei 7 Byte max. 4 usw.

    ps: Warum werden da überhaupt Handles verwendet? Ein Grund das zu machen wäre damit man den Heap "kompaktieren" kann. Also die einzelnen Objekte im Speicher rumzuschieben. Falls das gemacht wird, müsstest du das als erstes mal deaktivieren/entfernen. Denn du kannst C++ Objekte nicht einfach so im Speicher rumschieben.



  • @hustbaer

    Die beste Lösung wäre wohl parallel zu Heap_Alloc noch ein Heap_Alloc_Aligned zu implementieren, dass du dann ala Heap_Alloc_Aligned(sizeof(T), alignof(T)) aufrufst. Heap_Alloc_Aligned müsste dann garantieren Speicher zurückzuliefern der passend aligned ist. Und natürlich musst du es so machen, dass Heap_Data noch mit den von Heap_Alloc_Aligned zurückgelieferten Handles funktioniert.

    Danke für die Info, ich werde es so ausprobieren.

    Warum werden da überhaupt Handles verwendet? Ein Grund das zu machen wäre damit man den Heap "kompaktieren" kann.

    Das sind Sünden aus der Vergangenheit. Man hat auf einem kleinen Embedded Controller eine eigene Speicherverwaltung inklusive Kompaktierung implementiert und diese dann eins zu eins in einem Windows-Projekt übernommen.

    Und nun hat sich die Datenmenge von 4 MByte auf bis zu 1 GByte vergrößert.

    Ich verstehe nicht was du meinst. Wegen welcher Art der Initialisierung?

    Naja, die Initialisierung funktioniert hier nach dem Motto X Bytes allokieren und danach die Bytes als Zeiger auf Struct XYZ interpretieren. Und dies funktioniert halt nicht in C++ mit Klassen, da die Konstruktoraufrufe fehlen.

    Meinst du du willst das Heap_Alloc für MeasurementFolder beibehalten und MeasurementFolder dann mit placement new in dem per Heap_Alloc besorgten Speicher konstruieren?

    Ja so war mein Gedanke.

    Dadurch kann ich hoffentlich alle C Datenstrukturen von MeasurementFolder durch STL Varianten ersetzen und viele Handles zum Teufel jagen. Im zweiten Schritt kann ich mich dann um die restlichen Handles kümmern.

    Es ist halt wie schon gesagt eine etwas größere Baustelle.



  • @Quiche-Lorraine sagte in alignas bei placement new:

    Warum werden da überhaupt Handles verwendet? Ein Grund das zu machen wäre damit man den Heap "kompaktieren" kann.

    Das sind Sünden aus der Vergangenheit. Man hat auf einem kleinen Embedded Controller eine eigene Speicherverwaltung inklusive Kompaktierung implementiert und diese dann eins zu eins in einem Windows-Projekt übernommen.

    Und nun hat sich die Datenmenge von 4 MByte auf bis zu 1 GByte vergrößert.

    OK. Aber auf Windows wird jetzt nicht mehr kompaktiert, richtig?

    Ich verstehe nicht was du meinst. Wegen welcher Art der Initialisierung?

    Naja, die Initialisierung funktioniert hier nach dem Motto X Bytes allokieren und danach die Bytes als Zeiger auf Struct XYZ interpretieren. Und dies funktioniert halt nicht in C++ mit Klassen, da die Konstruktoraufrufe fehlen.

    Das alleine wäre doch kein Grund. So lange du keine mit dem alten System kompatiblen Handles brauchst kannst du die neuen Klassen/structs doch einfach ganz normal mit new erzeugen.

    Meinst du du willst das Heap_Alloc für MeasurementFolder beibehalten und MeasurementFolder dann mit placement new in dem per Heap_Alloc besorgten Speicher konstruieren?

    Ja so war mein Gedanke.

    OK, hier liegt der Hund begraben.
    Aber doofe Frage dazu: wie wird denn das Zeug wieder zerstört? Ich meine, wenn du da jetzt C++ Strings & Co reinklopfst, dann musst du ja auch irgendwo den Destruktor aufrufen. Beisst sich das nicht mit der Verwaltung per Handle?

    Es ist halt wie schon gesagt eine etwas größere Baustelle.

    Kann ich mir vorstellen 🙂



  • Vielleicht wäre es der bessere Weg erstmal die Handles loszuwerden indem du sie einfach durch rohe Zeiger ersetzt. Bei der Umstellung wird dann bereits das Heap_Alloc zu new geändert. (Ganze normales new, ohne "placement".)

    Und wenn die Handles alle weg sind, kannst du super easy den Rest ändern. Ohne dass du dabei mit placement-new, alignment oder sonstwas rummachen musst. Inklusive Ändern der rohen Zeiger in Smart-Pointer.



  • @Quiche-Lorraine sagte in alignas bei placement new:

    Auf vielen OS kann man es sich sparen normale Datentypen per explizites Aligment zu allokieren, aber das ist aber eine Eigenschaft des OS und nicht von C oder C++. Des weiteren gibt es eine Reihe von Gründen darüber hinausgehende Anforderungen ans Aligment zu stellen z.B. SIMD Erweiterung. Nimmt man die größeren 128Bit, 256Bit, 512Bit, … Datentypen für die jeweilige SIMD-Erweiterung wird das Aligment automatisch angepasst, castet man von float, double o.ä. dann halt nicht. Je nach Plattform läuft das Programm langsamer oder crasht.

    Naja, die Initialisierung funktioniert hier nach dem Motto X Bytes allokieren und danach die Bytes als Zeiger auf Struct XYZ interpretieren. Und dies funktioniert halt nicht in C++ mit Klassen, da die Konstruktoraufrufe fehlen.

    Placement new ist da nur bedingt eine Lösung. Du musst ohnehin die Konstruktoren sauber einbauen (d.h. von init Funktion auf Konstruktor umschreiben), und wenn Du das gemacht hast kannst Du gleich new verwenden.



  • @hustbaer sagte in alignas bei placement new:

    OK. Aber auf Windows wird jetzt nicht mehr kompaktiert, richtig?

    Exakt.

    Ich meine, wenn du da jetzt C++ Strings & Co reinklopfst, dann musst du ja auch irgendwo den Destruktor aufrufen. Beisst sich das nicht mit der Verwaltung per Handle?

    Stimmt, ich müsste da typisieren um den Konstruktor aufrufen zu können.

    Das würde eher für eine eigene Verwaltung in den Folder Funktionen sprechen. Dort gibt es nur zwei Typen (SubFolder und MeasurementFolder), wäre also machbar. Ich müsste bloß aufpassen das ich mit den so gewonnenen Handles nicht inkompatible Funktionen aufrufe. Evt. müsste ich bei den Folder-Funktionen also einen eigenen Handle-Typ einführen, welcher inkompatibel zu dem ersten Typ ist, damit mir der Compiler bei den richtigen Stellen auf die Finger klopft.

    Nicht einfach die Sache, aber ich glaube dieser Ansatz wäre ein Versuch wert.

    Damit hätte sich auch die Frage nach dem placement new erledigt. Danke.



  • @john-0 sagte in alignas bei placement new:

    Placement new ist da nur bedingt eine Lösung. Du musst ohnehin die Konstruktoren sauber einbauen (d.h. von init Funktion auf Konstruktor umschreiben), und wenn Du das gemacht hast kannst Du gleich new verwenden.

    Das Problem ist halt noch dieser alte C Code, welcher ich portieren möchte.

    Ich habe da eine C Implementierung von Heap-Funktionen (Alloc, Free), welche auf einem Embedded Controller genutzt wurde, wo new und malloc unbekannt waren. Und diese Funktionen werden noch von meinen Folder-Funktionen benutzt.

    Dank der Frage von @hustbaer bezüglich wer den Konstruktor aufruft, glaube ich muss diese Abhängigkeit zuerst getrennt werden. Die Folder-Instanzen dürfen nicht die Heap-Funktionen nutzen. Also etwas in die Richtung:

    using Entry = std::variant<SubFolder, MeasurementFolder>;
    
    std::map<Handle, Entry> gFolderEntries; 
    Handle gNextHandle = 1;
    
    // Folder-Funktionen deren Header ich im ersten Schritt nicht sonderlich ändern möchte
    Handle Folder_Create(char const* const Name, bool Subfolder) {
        if (Subfolder)
        {
            SubFolder f;
            
            gFolderEntries.insert(gNextHandle, f);
        }
        else
        {
            MeasurementFolder f;
            
            gFolderEntries.insert(gNextHandle, f);
        }    
        gNextHandle++;
        return h;
    }
    void Folder_Free(Handle h) {}
    

    Und erst dann kann ich SubFolder und MeasurementFolder auf C++ umstellen.


Log in to reply