Placement new[]

Discussion in 'C++' started by Joe The Smoe, Jan 30, 2014.

  1. Joe The Smoe

    Joe The Smoe Guest

    Hi,

    to manage memory allocation for a multi-threaded application and reduce
    the use of mutex, each thread is given an object of a class named "pool"
    which allocates, recycles and eventually releases a set of memory
    blocks (the classical memory pool).

    The different objects allocated by each thread can now be allocated
    using the given pool object thanks to a placement new and new[]
    operators given to each concerned class.

    The problem I meet is that I have something working pretty well except
    for the placement new[] which places the first element of the array 4
    bytes after the address returned by the new[] operator. This causes me
    some problem at the time the array has to be released because the memory
    address of the array (the address of the first element) does not match
    the address given by the pool to the placement new[] operator (for the
    pool this is not a memory block that it has allocated).

    OK, I could shift the array address by four bytes and it would work but,
    I'd rather like to understand.

    Thanks for any enlightenment! :)

    Cheers,
    Joe.

    P.S: the placement new, new[], delete, and delete[] operators are
    textually identical for each class, I can use a #define macro to add
    them to each class. But, is there a better C++ way of doing that?
     
    Joe The Smoe, Jan 30, 2014
    #1
    1. Advertisements

  2. Joe The Smoe schreef op 30-Jan-14 9:18 AM:
    Ordinary new and delete deal with a pointer to a fixed type, hence known
    size of the object (the compiler can supply the size to the delete call).

    Not so for the delete[] operator: it gets a pointer to the first entry,
    but there can be just that one entry, but it could equally be a
    1000-entry array. There is no guaranteed way for the compiler to known
    this at the delete. Hence a common implementation of new[] puts the size
    in front of the allocated block of memory, so delete[] can find it and
    knows the size of the block.
    Inherit from a base class?

    Wouter
     
    Wouter van Ooijen, Jan 30, 2014
    #2
    1. Advertisements

  3. Am 30.01.2014 09:18, schrieb Joe The Smoe:
    This is a quite typical strategy, namely to be able to record the number
    of elements the corresponding delete[] must destroy.

    How, actually, are you releasing the memory? If you delete the objects
    by delete[], as you should (and not delete, without the brackets), you
    should get the right memory address, adjusted with the offset. If this
    is not the case, it is possibly a compiler bug (g++ does *not* show this
    bug).

    Note well you need to provide four operator deletes:

    operator delete(void *);
    deletes objects. You need to find your pool from the object. This delete
    is used for regular delete in the program flow, even if deleting objects
    from the pool.

    operator delete[](void *);
    Used for regular array destruction.

    operator delete(void *,Pool);
    used if an operator-new fails in the construction of an object in the
    initializer list

    operator delete[](void *,Pool);
    the same for arrays.

    Note specifically that your regular delete operator does *not* get the
    pool passed in, even if it needs it. It's up to your new()
    implementation to store it in an appropriate place if required. Plus,
    "placement new" does not match "placement delete" as one may think.
    "placement new" is complemented by "regular delete" plus "placement delete".

    Greetings,
    Thomas
     
    Thomas Richter, Jan 30, 2014
    #3
  4. Joe The Smoe

    Joe The Smoe Guest

    Le 30/01/2014 09:38, Wouter van Ooijen a écrit :
    OK, I understand.

    So, my implementation of the class pool is probably wrong. I intended to
    put a pointer to the allocating pool object just before the allocated
    block asked by a placement new/new[] in order to allow only code
    modification by replacing the new operator by a it placement's
    counterpart, keeping the delete invocation equal:

    // adding a thread local pool object
    pool p();

    // using placement new in place of usual new operator
    toto *ptr = new (p) toto(3);

    ... (doing something with ptr)

    // but object get deleted normally (which that's the point is
    // not the common way of doing, I admit...)
    delete ptr;

    where the delete operator for the class toto is taken from its parent
    class "on_pool" --- thanks for the hint :) :

    class on_pool
    {
    public:
    // get_pool() returns the address of the allocating pool
    // for "this" by fetching the field placed before
    // the object's address: "this - sizeof(pool *)" bytes
    pool & get_pool() const
    { return **((pool **)(pool::shift_left((void *)this))); };

    // as expected the placement new operator
    void *operator new(size_t n_byte, pool & p)
    { return p.alloc(n_byte); };

    // and its associated "on-exception" delete operator
    void operator delete(void *ptr, pool & p)
    { p.dealloc(ptr); };

    // the delete operator overwritten to release address
    // for the associated pool
    void operator delete(void *ptr)
    { ((on_pool *)ptr)->get_pool().dealloc(ptr); };

    // this works fine, but doing the same for placement new[]
    // and the on_pool::delete[] operators fails because
    // delete[] should not be used against object allocated
    // by a placement new[] ... I'm hitting compiler internal's
    // implementation of new[] and delete[] default operators.
    };


    class toto: public on_pool
    {
    ...
    };


    Sight ... :-(

    I wished I could abstract well the use of placement new in the current
    code, but it seems I cannot avoid directly calling the destructor and
    then the delete[] operator. Which to my humble point of view brings a
    big difference about the readability of the code and which is source of
    error (delete without prior desctructor call, and viceversa),

    All the block recycling is done by the pool class not
    by the caller (allocation/release abstraction) but I cannot abstract for
    the caller the fact that the memory is allocated on the pool by just the
    slightly modifying the "new" invocation...

    *Sight*

    Yes, I stupidly thought the new and delete operator where not
    inheritable... Just tested it and it perfectly works! Thanks. :)
     
    Joe The Smoe, Jan 30, 2014
    #4
  5. Am 30.01.2014 14:03, schrieb Joe The Smoe:
    ....

    Couple of problems with this code, namely the alignment might not be
    right. But if operator new[] and operator delete[] are implemented in
    the same way, then *that* part should be correct. I'm a bit too lazy to
    try your code with g++, but I'll just share mine if you want to take it.

    Here's an implementation that works with g++: What I show here is the
    class you need to inherit from to be able to "pool allocate" any
    derived classes.

    class JObject {
    private:
    // Make Operator New without arguments private here.
    // They must not be used.
    void *operator new[](size_t)
    {
    assert(false);
    return (void *)1; // GNU doesn't accept NULL here. This should
    throw as well.
    }
    void *operator new(size_t)
    {
    assert(false);
    return (void *)1;
    }
    //
    // Private structure for effective arena-new management:
    //
    // Due to bugs in both the GCC and the VS, we must keep the
    // size in the memory holder as well. The size parameter of
    // the delete operator is just broken (a little broken on GCC,
    // a lot broken on VS).
    // This is still slightly more effective than using AllocVec
    // here.
    // *BIG SIGH*
    union MemoryHolder {
    // This union here enforces alignment
    // of the data.
    struct {
    // back-pointer to the environment
    class Environ *mh_pEnviron;
    size_t mh_ulSize;
    } mh;
    // The following are here strictly for alignment
    // reasons.
    union Environ::Align mh_Align;
    };
    //
    public:
    // An overloaded "new" operator to allocate memory from the
    // environment.
    static void *operator new[](size_t size,class Environ *env)
    {
    union MemoryHolder *mem;
    // Keep the memory holder here as well
    size += sizeof(union MemoryHolder);
    // Now allocate the memory from the environment.
    mem = (union MemoryHolder *)env->AllocMem(size);
    // Install the environment pointer and (possibly) the size
    mem->mh.mh_pEnviron = env;
    mem->mh.mh_ulSize = size;
    return (void *)(mem + 1);
    }
    //
    static void *operator new(size_t size,class Environ *env)
    {
    union MemoryHolder *mem;
    // Keep the memory holder here as well
    size += sizeof(union MemoryHolder);
    // Now allocate the memory from the environment.
    mem = (union MemoryHolder *)env->AllocMem(size);
    // Install the environment pointer and (possibly) the size
    mem->mh.mh_pEnviron = env;
    mem->mh.mh_ulSize = size;
    return (void *)(mem + 1);
    }
    //
    //
    // An overloaded "delete" operator to remove memory from the
    // environment.
    static void operator delete[](void *obj)
    {
    if (obj) {
    union MemoryHolder *mem = ((union MemoryHolder *)(obj)) - 1;
    mem->mh.mh_pEnviron->FreeMem(mem,mem->mh.mh_ulSize);
    }
    }
    //
    //
    static void operator delete(void *obj)
    {
    if (obj) {
    union MemoryHolder *mem = ((union MemoryHolder *)(obj)) - 1;
    mem->mh.mh_pEnviron->FreeMem(mem,mem->mh.mh_ulSize);
    }
    }
    };

    The "Environment" is what your pool is. It provides functions AllocMem()
    and FreeMem() that handle memory. Both require a pointer and a size. It
    also contains a small union of the following type:

    union Align {
    UBYTE a_byte;
    UWORD a_word;
    ULONG a_int;
    FLOAT a_float;
    DOUBLE a_double;
    UQUAD a_quad;
    APTR a_ptr;
    };

    that is just there to ensure correct alignment of all types. The UPPER
    CASE names there are environment specific types and set accordingly,
    using autoconf.

    The construction works fine with g++, and VS, though a couple of bugs
    were present that required me to keep the size in the allocation itself
    because the compiler provided value was, for some versions of the
    compilers, incorrect.

    To use it, first derive from JObject:

    class bla : public JObject {
    ...
    }

    and then:

    foo = new(environ) bla(...);

    delete foo;

    does what it should do, including arrays (tested). If it does not,
    you're mixing new[] with delete, or new with delete[].

    HTHH,
    Thomas
     
    Thomas Richter, Jan 30, 2014
    #5
  6. Joe The Smoe

    Joe The Smoe Guest

    [...]

    Hello Thomas,
    after having carefully looked at your implementation --- thank you very
    much! :) --- put apart the lack of alignment --- thanks again! --- I did
    not understood why your implementation should be working while mine was
    not, seen the many similarities... Well, it was evident... I was using
    delete in place of delete[] for the array... I should have seen it
    before posting... I feel ashamed... However I'm happy having asked as
    else I would have missed the alignment consideration :)
    No need to try it, it works as expected... if you don't mix new[] with
    delete and new with delete[]... as I did :)

    By the way I was wondering why you forbade the normal new[]() and new()
    operators putting them as private:

    It could be possible to allow them by setting the value of the pointer
    on class Environ in the MemoryHolder data to NULL. Memory allocation
    could be then done by operator ::new char[size]. Then, to known how to
    delete/delete[] the data it would just require to check whether the
    mh_pEnviron field is NULL (use ::delete[]) or not (release calling the
    Environ object as actually done), no?
    Here, I cannot figure out why you need the size to release a memory
    block... but that's probably because of the class Environ
    implementation, right?

    [...]
    Yes, I was.
    Thanks again,
    Joe.
     
    Joe The Smoe, Jan 31, 2014
    #6
    1. Advertisements

Ask a Question

Want to reply to this thread or ask your own question?

You'll need to choose a username for the site, which only take a couple of moments (here). After that, you can post your question and our members will help you out.