alignment requirements



  • Hi!

    Hintergrund: ich möchte Platz für 2 UDTs mit einem "new" anfordern, und die Typen danach "mit Hand hineinkonstruieren". Grund für den ganzen Tanz ist dass "new" bei der speziellen Operation einen grossen Teil der Zeit frisst, und ich möchte einfach 2x new auf 1x new zusammenfassen. Wäre ne tolle Sache. Blöderweise ist zumindest ein Typ an der Stelle nicht bekannt (die Grösse könnte ich von einer Factory bekommen auf die ich einen abstrakten Basisklassen-Zeiger habe). Blöderweise ist keiner der beiden Typen "konstant", der eine kommt eben aus einer Factory (und kann daher je nach eingesetzter Factory-Instanz wechseln), der andere ist ein Funktor der an ein Template übergeben wird -- kann also bei jeder Instanzierung des Templates anders sein.

    Frage #1: Gibt es eine einfache Möglichkeit mit Standard C++ (d.h. auch Plattformunabhängig) draufzukommen welches Alignment für einen bestimmten Typ T (der eben ein UDT sein kann) benötigt wird, von dem nur sizeof(T) bekannt ist? Ich brauche auch nicht den genauen Wert, ein vielfaches davon wäre auch OK, solange es nicht gerade in den Kilobyte-Bereich geht.

    Eine einfache Möglichkeit wäre natürlich anzunehmen "required_alignment(T) == sizeof(T)". Das muss ja AFAIK immer passen, aber das wäre bei grossem sizeof(T) ziemliche Platzverschwendung - ich muss ja "required_alignment - 1" mehr Bytes anfordern damit ich das Alignment mit Hand hinbiegen kann.

    Frage #2:

    Gibt es eine (einfache) Möglichkeit das rauszubekommen wenn T vollständig bekannt ist? In dem Fall müsste ich eben hergehen und die Factory (die T ja kennt) auch nach dem Alignment fragen. Würde ich aber gerne vermeiden...

    Frage #3:

    Es wird ja soweit ich das verstanden habe garantiert dass "new char[size]" immer einen Zeiger zurückliefert wo ich jedes Objekt mit "sizeof(T) == size" hineinkonstuieren kann, also mit passendem Alignment halt. Gilt das nur für "sizeof(T) == size", oder generell für "sizeof(T) <= size"?

    Und #4:

    Warum zum Geier muss sowas in C++ so verflixt kompliziert sein? Neben einem sizeof(T) könnte ein alignof(T) ja wirklich nicht schaden -- und schwer zu implementieren wäre es auch nicht. Und gleich noch eine "alignment_from_size()" Funktion dazu. *grmpf*

    Lieben Dank,
    hustbaer


  • Mod

    hustbaer schrieb:

    Hi!

    Hintergrund: ich möchte Platz für 2 UDTs mit einem "new" anfordern, und die Typen danach "mit Hand hineinkonstruieren". Grund für den ganzen Tanz ist dass "new" bei der speziellen Operation einen grossen Teil der Zeit frisst, und ich möchte einfach 2x new auf 1x new zusammenfassen. Wäre ne tolle Sache. Blöderweise ist zumindest ein Typ an der Stelle nicht bekannt (die Grösse könnte ich von einer Factory bekommen auf die ich einen abstrakten Basisklassen-Zeiger habe). Blöderweise ist keiner der beiden Typen "konstant", der eine kommt eben aus einer Factory (und kann daher je nach eingesetzter Factory-Instanz wechseln), der andere ist ein Funktor der an ein Template übergeben wird -- kann also bei jeder Instanzierung des Templates anders sein.

    Frage #1: Gibt es eine einfache Möglichkeit mit Standard C++ (d.h. auch Plattformunabhängig) draufzukommen welches Alignment für einen bestimmten Typ T (der eben ein UDT sein kann) benötigt wird, von dem nur sizeof(T) bekannt ist? Ich brauche auch nicht den genauen Wert, ein vielfaches davon wäre auch OK, solange es nicht gerade in den Kilobyte-Bereich geht.

    Das geht noch relativ einfach, boosts oder tr1' type_traits machen es vor, im Zweifel kannst du dir die Quellen anschauen. Prinzip:

    template<typename T> struct Foo { char c; T t; Foo(); };
    template<typename T> struct alignment_of { static const size_t value = sizeof( Foo< T > ) - sizeof( T ); };
    

    Das Ergebnis ist garantiert ein Vielfaches des gesuchten Alignments und wohl für jeden existierenden Compiler (zumindest für die, welche boost unterstützen) mit jenem identisch.

    Problematisch ist eher, dass es keine Möglichkeit gibt, herauszufinden, ob ein gegebener char oder void-pointer ausreichend ausgerichtet für ein T ist, bzw. wie dieser zu verändern ist, um korrekte Ausrichtung zu erreichen. Üblicherweise macht man das ja durch cast in ein Integer - dessen Verhalten ist aber von vornherein durch die Implementation definiert.

    hustbaer schrieb:

    Frage #2:

    Gibt es eine (einfache) Möglichkeit das rauszubekommen wenn T vollständig bekannt ist? In dem Fall müsste ich eben hergehen und die Factory (die T ja kennt) auch nach dem Alignment fragen. Würde ich aber gerne vermeiden...

    Für sizeof( T) muss T ohnehin vollständig sein, also siehe Frage #1

    hustbaer schrieb:

    Frage #3:

    Es wird ja soweit ich das verstanden habe garantiert dass "new char[size]" immer einen Zeiger zurückliefert wo ich jedes Objekt mit "sizeof(T) == size" hineinkonstuieren kann, also mit passendem Alignment halt. Gilt das nur für "sizeof(T) == size", oder generell für "sizeof(T) <= size"?

    Der zurückgegeben Pointer kann in jeden beliebigen Pointer auf einen vollständigen Typ umgewandelt werden. Sogar dann, wenn der angeforderte Speicher nicht groß genug ist, um ein Objekt dieses Typs darin zu konstruieren. Das impliziert beispielsweise, dass man Alexandrescus Small-Object-Allocator nicht für die den operator new einsetzen darf.

    Und #4:

    Warum zum Geier muss sowas in C++ so verflixt kompliziert sein? Neben einem sizeof(T) könnte ein alignof(T) ja wirklich nicht schaden -- und schwer zu implementieren wäre es auch nicht. Und gleich noch eine "alignment_from_size()" Funktion dazu. *grmpf*

    Manches kann man mit den type traits machen. Anderes wird in C++0x besser werden. Vieles wird problematisch bleiben. 🙂



  • @camper:
    Ok, danke erstmal.

    Dass für sizeof(T), T erstmal vollständig bekannt sein muss, ist mir schon klar. Die Idee wäre gewesen eben nur sizeof(T) (über eine virtuelle Funktion) an einen anderen Programmteil zu übergeben, der dann mit einem "new char[]" Platz für ein T und noch ein anderes Objekt (sagen wir vom Typ U) anfordern soll. Wobei eben T zur Laufzeit variabel ist, und U ein Template Parameter. An der Stelle wo Speicher angefordert werden soll ist also nur U bekannt, T aber nicht.

    Ich werde mir aber überlegen ob es nicht akzeptabel wäre beide Typen dem Template bekannt zu machen, dann fällt das ganze Problem weg, und ich spare mir auch noch einige calls über function Pointer und 1-2 virtual calls, die ja beide auch nicht ganz gratis sind.

    Dadurch fällt zwar auch der runtime Polymorphismus weg (den ich eigentlich haben wollte), aber naja.

    Der zurückgegeben Pointer kann in jeden beliebigen Pointer auf einen vollständigen Typ umgewandelt werden. Sogar dann, wenn der angeforderte Speicher nicht groß genug ist, um ein Objekt dieses Typs darin zu konstruieren.

    Hä? Ich verstehe nicht ganz was du damit meinst...
    Ich meide dass sowas AFAIK laut Standard OK ist, also zu keinen Problem/undefiniertem Verhalten führen darf:

    class X
    {
        // ...
    };
    
    void foo()
    {
        void* p = new char[sizeof(X)];
        static_cast<X*>(p)->X::X();
    
        // oder sogar so:
        void* p2 = new char[sizeof(X)*6];
        (static_cast<X*>(p2) + 5)->X::X();
    
        // oder so:
        struct POD
        {
            char s1[sizeof(X)*2];
        };
    
        POD pod;
        X* p3 = reinterpret_cast<X*>(&pod);
        p3[1]->X::X();
        p3[1]->X::~X();
    }
    

    Die Frage ist bloss, ob das laut Standard auch OK ist:

    class X
    {
        // ...
    };
    
    void foo()
    {
        void* p = new char[sizeof(X) + 1]; // wenn sizeof(X) != 1
        static_cast<X*>(p)->X::X();
    }
    

  • Mod

    hustbaer schrieb:

    @camper:
    Ok, danke erstmal.

    Dass für sizeof(T), T erstmal vollständig bekannt sein muss, ist mir schon klar. Die Idee wäre gewesen eben nur sizeof(T) (über eine virtuelle Funktion) an einen anderen Programmteil zu übergeben, der dann mit einem "new char[]" Platz für ein T und noch ein anderes Objekt (sagen wir vom Typ U) anfordern soll. Wobei eben T zur Laufzeit variabel ist, und U ein Template Parameter. An der Stelle wo Speicher angefordert werden soll ist also nur U bekannt, T aber nicht.

    Ich werde mir aber überlegen ob es nicht akzeptabel wäre beide Typen dem Template bekannt zu machen, dann fällt das ganze Problem weg, und ich spare mir auch noch einige calls über function Pointer und 1-2 virtual calls, die ja beide auch nicht ganz gratis sind.

    Dadurch fällt zwar auch der runtime Polymorphismus weg (den ich eigentlich haben wollte), aber naja.

    Es genügt ja, dass strikteste Alignment (bzw. einen Typ dazu) deiner Plattform zu ermitteln und das Objekt vom Typ T entsprechend ausgerichtet zu konstruieren. Ich sehe allerdings keinen Weg, dieses strikteste Alignment direkt zu ermitteln - ein möglicher Weg könnte darin bestehen, alle primtiven Datentypen durchzuprobieren, aber das müsste man sich noch besser überlegen...

    hustbaer schrieb:

    Der zurückgegeben Pointer kann in jeden beliebigen Pointer auf einen vollständigen Typ umgewandelt werden. Sogar dann, wenn der angeforderte Speicher nicht groß genug ist, um ein Objekt dieses Typs darin zu konstruieren.

    Hä? Ich verstehe nicht ganz was du damit meinst...

    Du hast schon verstanden. Der zurückgegeben Pointer ist immer korrekt für jeden Datentyp ausgerichtet. Sogar dann, wenn der angeforderte Speicher nicht groß genug für diesen Typ ist.

    Ich meide dass sowas AFAIK laut Standard OK ist, also zu keinen Problem/undefiniertem Verhalten führen darf:

    class X
    {
        // ...
    };
    
    void foo()
    {
        void* p = new char[sizeof(X)];
        static_cast<X*>(p)->X::X();
    
        // oder sogar so:
        void* p2 = new char[sizeof(X)*6];
        (static_cast<X*>(p2) + 5)->X::X();
    
        // oder so:
        struct POD
        {
            char s1[sizeof(X)*2];
        };
    
        POD pod;
        X* p3 = reinterpret_cast<X*>(&pod);
        p3[1]->X::X();
        p3[1]->X::~X();
    }
    

    Die Frage ist bloss, ob das laut Standard auch OK ist:

    class X
    {
        // ...
    };
    
    void foo()
    {
        void* p = new char[sizeof(X) + 1]; // wenn sizeof(X) != 1
        static_cast<X*>(p)->X::X();
    }
    

    Das könnte undefiniertes Verhalten sein, allerdings nicht wegen fehlender Ausrichtung. Ein static_cast in T* auf ein void*, dass durch implizite Konvertierung aus einem char* entstanden ist ... für jede andere Variante ist das undefiniert, bei char* wird die Welt da grau (heißt, ich bin mir nicht so sicher) ...
    Jedenfalls besteht keine Notwendigkeit, dass so zu machen, zudem ist nicht klar, wie

    static_cast<X*>(p)->X::X();
    

    funtkionieren soll. Allemal haben wir reinterpret_cast und placement new, um genau auszudrücken, was wir wollen:

    void foo()
    {
        char* p = new char[sizeof(X)];
        new(p) X();
    
        // oder sogar so:
        char* p2 = new char[sizeof(X)*6];
        new(p2+5*sizeof(X)) X(); // oder
        new(reinterpret_cast<X*>(p2)+5) X();
    
        // oder so:
        struct POD
        {
            char s1[sizeof(X)*2];
        };
    
        POD pod;
        X* p3 = reinterpret_cast<X*>(&pod); // Das geht nun gerade nicht. 
        // Für das Objekt pod wird keine Allokationsfunktion aufgerufen und die
        // Ausrichtung von pod muss nur den Anforderungen eines chars genügen.
    }
    


  • Hab grad nicht SO viel Zeit, deswegen nur kurz: wieso sollte "static_cast<X*>(p)->X::X()" ein Problem sein?
    Verstehe ich nicht...



  • hustbaer schrieb:

    Hab grad nicht SO viel Zeit, deswegen nur kurz: wieso sollte "static_cast<X*>(p)->X::X()" ein Problem sein?
    Verstehe ich nicht...

    Hauptsächlich, weil es kein legales C++ ist. Im Gegensatz zum Destruktor kann ein Konstruktor nicht direkt aufgerufen werden.



  • HumeSikkins schrieb:

    hustbaer schrieb:

    Hab grad nicht SO viel Zeit, deswegen nur kurz: wieso sollte "static_cast<X*>(p)->X::X()" ein Problem sein?
    Verstehe ich nicht...

    Hauptsächlich, weil es kein legales C++ ist. Im Gegensatz zum Destruktor kann ein Konstruktor nicht direkt aufgerufen werden.

    So isset, genau deshalb gibt es ja placement new!



  • Das is aber auch alles etwas verworren...
    Naja.
    Ok, habs jetzt mitm Comeau probiert der frisst ->X::X() nicht 😞
    MSVC tut das nämlich...


Log in to reply