Ist das Data-Aligment bei placement-new ein Problem?



  • Ich grüße alle C++ Fans ... und hab auch ein Frage an euch. 😃

    Nach vielen Performance-Tests komme ich nun zum Schluß: Man soll nicht unnötig Resourcen verschwenden ... na klar.
    Daher habe ich mir ein paar templates gebastelt um möglichst effizient mit Speicher umzugehen. Damit meine ich vor allem reine "new"s reduzieren und Objekte auf bereits vorhandenem Speicher per placment-new zu erzeugen (macht die STL ja auch gerne). Obwohl ich bisher keine Probleme damit habe, frage ich mich ob es bei solchen Verfahren Probleme mit dem Aligment der Daten geben kann und wie man möglichst system-unabhängigen Code dafür schreiben kann.

    Aktuellstes Beispiel:

    template<class T> class LocalRef
    {
    private:
      char	m_mem[sizeof(T) + 1];
    public:
      ...
      LocalRef() { m_mem[sizeof(T)] = 0; }
      LocalRef(const T & r) {
        new(m_mem) T();
        m_mem[sizeof(T)] = 1;
      }
      T& operator*() {
        if(!m_mem[sizeof(T)]) throw UninitializedException();
        return *reinterpret_cast<T*>(m_mem);
      }
      ...
    };
    

    Auf dem Speicher in m_mem kann ein Objekt per new(m_mem) T() erzeugt werden. Das letzte Byte dient als flag, das anzeigt, ob bereits ein Objekt konstruiert wurde oder nicht.

    Ich kenne nur ein paar Grundsätze zum Alignment, habe aber keine praktische Erfahrung damit. Meines Wissens ist es vor allem für RISC CPUs ein Problem einzelne Bytes zu lesen, weshalb der Compiler gerne in Architektur-spezifischen WORD-Schritten Objekte im Speicher anlegt.

    Könnte daher mein Beispiel, wo ich ein letztes zusätzliches Byte als Flag hinten anhänge, ein undefiniertes Verhalten erzeugen?
    zB:

    struct Color { char R; char G; char B; Color(...) ..... };
    LocalRef<Color> col;
    ...
    *col = Color(0,0,0);
    

    Könnte hier der Compiler auf die Idee kommen gleich 4 Bytes zu überschreiben (und somit mein Flag killen)?
    Sollte man das Flag als eigenen Member implementieren (und dann gleich als int wegen Alignment) ??? 😕

    Sorry für dieses etwas abstrakte Beispiel, aber ich möchte hier evaluieren, wie sinnvoll es ist das Speicher-Management zu beinflussen... die Performance auf nem x86-PC ist einem auto_ptr/shared_ptr überlegen, stellt sich bloß die Frage, ob man sich später bei der Portierung des Codes fürs Handy (zB ARM) Probleme einhandelt.
    Ich habe auch noch andere Anwendung ähnlicher Art, etwa ein reference-counted-fixed-length Array das ebenfalls in einem einzigen Speicherblock liegt. Auch da frage ich mich ob hier ein Member den anderen Überschreiben kann...

    lg XOR 🙂



  • Per Definition ist char das kleinste, was man adressieren kann. Da wird auch nichts überschrieben, von Deinem Flag. Aber sicher ist die Sache trotzdem nicht. Der Typ LocalRef<T> hat hier immer ein "alignment requirement" von 1 (Du speicherst ja nur ein char-Array). T kann aber ein größeres "alignment requirement" haben. Dann würdest Du beim reinterpret_cast einen ungültigen Zeiger des Typs T* erzeugen.

    Es sieht ein bisschen so aus wie boost::optional, was Du da versuchst. Aber ich sehe jetzt nicht, wie das Performance-technisch helfen soll. Ich denke, grenzt gefährlich nah an "premature optimization".



  • Ui, das ist mal ne Frage...hast du nen Standard zur Hand? Sonst wird das schwer nachzuvollziehen.

    Also, nach 5.3.4 (14) nimmt der new-Ausdruck an, dass der Speicher, der von der Allokationsfunktion zurückgegeben wird, richtig ausgerichtet ist. In deinem Fall ist die Allokationsfunktion die Placement-Form von operator new (void *operator new(std::size_t, void*)) nach 18.4.1.3, die einfach nur die übergebene Adresse zurückgibt - wenn die also nicht richtig ausgerichtet ist, hält diese Bedingung nicht, und der Konstruktor muss ggf. mit falsch ausgerichtetem Speicher arbeiten.

    Wenn ich das richtig lese, erzeugt es auf die Art, die du gerade verwendest, dementsprechend undefiniertes Verhalten; das scheint sich auch mit 5.3.4 (10) zu decken.

    Ich nehme an, dass boost.optional, welches krümelkacker ja schon erwähnt hat und das ziemlich genau das macht, was du da vorzuhaben scheinst, aus diesem Grund Code verwendet, der sicherstellt, dass sein char-Array richtig ausgerichtet ist.



  • 👍 Coole Sache ... und wieder einmal hätte meine Frage lauten sollen: Wie heißt die boost-Klasse für...? 😃

    Danke für eure Antworten :), und was ich gerade gelesen habe ist boost::optional genau das, was mir vorschwebte.
    Und der Code in "boost/type_traits/alignment_of.hpp" ist echt interessant. ...wielange wird es wohl dauern, bis ich alle boost Klassen durchhabe... 🙄

    Zur Erklärung: Ein älteres Projekt aus der VC 6.0 Zeit bereitete mir einige Sorgen nachdem mir ein Performance-Analyzer relativ viel CPU-Zeit für new und delete auswertete. Zugegeben, Schuld war die Objekt-Struktur, wo in jedem Konstruktor zahlreiche weitere Objekte erzeugt wurden, die oft gar nicht gebraucht wurden. Getoppt wurde das ganze von einem Daten-Index der den Speicher recht aufbläht und dann gerne das Pagefile beschäftigt.
    ... und man glaubt es kaum, was man mit ein paar smart-pointern und "on-demand-creation" für Verbesserungen bewirken kann. Nachdem ich aber weiter recherchiert hatte und ein paar Beiträge zum Thema Speicherfragmentierung gelesen hatte, begann ich eben mit jeder Menge Tests, wie man - mit vertretbarem Aufwand - Heapbedarf reduzieren kann. Von "Alignment" hatte ich noch nie etwas gehört, bis mich ein Artikel zu x64 und später eine "Alignment-Exception" in einem WindowsMobile Projekt wachgerüttelt hatte.

    Und gut, dass ihr mich gleich darauf hingewiesen habt, weil ich schon anfangen wollte, die "Optimierung" umzusetzen. 🙂

    lg XOR



  • xor schrieb:

    Zur Erklärung: Ein älteres Projekt aus der VC 6.0 Zeit bereitete mir einige Sorgen nachdem mir ein Performance-Analyzer relativ viel CPU-Zeit für new und delete auswertete. Zugegeben, Schuld war die Objekt-Struktur, wo in jedem Konstruktor zahlreiche weitere Objekte erzeugt wurden, die oft gar nicht gebraucht wurden. Getoppt wurde das ganze von einem Daten-Index der den Speicher recht aufbläht und dann gerne das Pagefile beschäftigt.
    ... und man glaubt es kaum, was man mit ein paar smart-pointern und "on-demand-creation" für Verbesserungen bewirken kann. Nachdem ich aber weiter recherchiert hatte und ein paar Beiträge zum Thema Speicherfragmentierung gelesen hatte, begann ich eben mit jeder Menge Tests, wie man - mit vertretbarem Aufwand - Heapbedarf reduzieren kann. Von "Alignment" hatte ich noch nie etwas gehört, bis mich ein Artikel zu x64 und später eine "Alignment-Exception" in einem WindowsMobile Projekt wachgerüttelt hatte.

    Zu den vielen new/delete-Aufrufen kann man sicher auch was mit memory-Pools drehen. Grade wenns nur eine Handvoll Klassen sind die immer wieder erzeugt werden kann man für die operator new und operator delete überladen und bei Bedarf dann gleich für einen ganzen Haufen davon Speicher reservieren statt für jedes einzeln.



  • Entweder war das Beispiel nur schlecht gewählt, oder er will wirklich Objekte von Typen wie "Color" per new anlegen. Ich denke, es lohnt sich, nochmal darüber nachzudenken, ob man solche Objekte nicht doch lieber im Automatischen Speicher anlegt oder direkt in Containern speichert, statt sie per new anzulegen...



  • Weil mich die boost-Lösung für korrektes Alignment fasziniert, poste ich mal, was ich gelernt habe ... also bitte mich zurechtweisen, wenns falsch ist 😃

    template<class T>
    struct alignment_hack
    {
      char c;
      T t;
    };
    

    Diese Hilfsklasse beinhaltet die notwendige Alignment-Lücke zwischen dem char und dem Typ T. Erfordert T ein 32-bit Alignment, so ordnet es der Compiler an Offset 4 ein. Mit dem Ausdruck
    sizeof(alignment_hack<T> - sizeof(T)) lässt sich also die Alignment-Grüße bestimmen.

    Wenn also für alle Größen ein template spezialisiert ist...
    (Folgendes ist nur ein primitivstes Beispiel, boost ist wesentlich vollständiger und compiler-unabhängiger)

    template<unsigned size> struct aligned_type { };
    
    template<> struct aligned_type<1> { typedef char type; };
    template<> struct aligned_type<2> { typedef short type; };
    template<> struct aligned_type<4> { typedef long type; };
    template<> struct aligned_type<8> { typedef long long type; };
    

    ..kann man für jedes mögliche T eine korrekt ausgerichteten Hilfsvariable anlegen.

    template<class T> struct aligned_storage
    {
      union
      {
        char	mem[sizeof(T)];
        typename aligned_type<sizeof(alignment_hack<T>) - sizeof(T)>::type aligned_var;
      };
    };
    

    Packt man diese Hilfsvariable mit einem char-Array in einen union ist der union auch für ein T richtig
    aligned ...

    Und dann sollte man ohne Probleme ein
    new (reinterpret_cast<void*>(store.mem)) T();
    bzw. ein reinterpret_cast<T*>(store.mem)->~T();
    ausführen können.

    Es ist schon übel. Ich hab früher in C oftmals ein char array irgend wo angelegt und dann dort
    wild strukturen rein und raus-gecastet zwecks einfacher Speicherung oder übertragung auf nem Socket. Und dann erfährt man so ganz nebenbei, dass das nur auf nem 32bit PC problemlos geht aber auf den meisten anderen CPUs Exceptions auslöst oder zumindest die Performance runterzieht ... ja man lernt eben nie aus.

    Fazit: boost rulez!

    Lg XOR

    @pumuckl:
    Ich frage mich, ob dem Urheber dieses Codes nicht bewusst war, dass man Daten auch auf dem Stack lokal anlegen kann: 😃 ... diese Member sind sowas von sinnlos.

    this->m_doc1 = new char[128];
    this->m_doc2 = new char[128];
    read_doc(this->m_doc1, 128, &this->m_doc1_len);
    process_data(this->m_doc1, this->m_doc1_len);
    delete[] this->m_doc1;
    delete[] this->m_doc2;
    
    this->m_doc1 = new char[128];
    this->m_doc2 = new char[128];
    read_doc(this->m_doc2, 128, &this->m_doc2_len);
    process_data(this->m_doc2, this->m_doc2_len);
    delete[] this->m_doc1;
    delete[] this->m_doc2;
    

    Sobald ich also diesen Kot weggemacht habe, komme ich gerne auf deinen Vorschlag mit der new Überladung zurück. ... Im Idealfall sind dann die meisten news gar nicht mehr vorhanden.

    @krümelkacker:
    Das Color Beispiel war von mir gewählt, weil hier 3 chars zusammengefasst werden, die Struktur aber ein größeres Alignment haben kann (etwa 4). sizeof(Color) kann dann je nach Einstellung variieren (3, 4, ...). Ich fragte mich, ob bei einem char m_mem[sizeof(Color) + 1] durch ein *reinterpret_cast<Color*>(m_mem) = Color(0,0,0); das 4. Byte auch mitüberschrieben werden kann, wenn sizeof(Color) 3 wäre ... aber dieses hinten angehängte Flag ist eh Schwachsinn und würde in einen eigenen Member kommen. (Ursprünglich wollte ich das Flag ja in das erste char setzen und placment-new mit nem Pointer auf das 2. char aufrufen ... aber das wäre ja noch viel viel böser gewesen 😉


Log in to reply