4

I would like to take advantage of the following advertised feature of boost::fast_pool_allocator (see the Boost documentation for Boost Pool):

For example, you could have a situation where you want to allocate a bunch of small objects at one point, and then reach a point in your program where none of them are needed any more. Using pool interfaces, you can choose to run their destructors or just drop them off into oblivion...

(See here for this quote.)

The key phrase is drop them off into oblivion. I do not want the destructors called on these objects.

(The reason is that I have millions of tiny objects that form an extremely complex web of ownership on the heap, and it takes my program about 20 minutes to call all of the destructors when the single parent-most object goes off the stack. I do not need these destructors called, because there are no desired side effects and all memory is contained within the boost::pool.)

Unfortunately, despite the promise of the above documentation, and the promise of the boost::pool concept, I cannot find a way to prevent the destructors of the managed objects from being called.

The problem is easily isolated in a small sample program:

class Obj
{
public:
    ~Obj()
    {
        // Placing a breakpoint here indicates that this is *always* reached
        // (except for the crash scenario discussed below)
        int m = 0;
    }
};

typedef std::map<int, Obj, std::less<int>,
                 boost::fast_pool_allocator<std::pair<int const, Obj>>>
        fast_int_to_int_map;

class Foo
{
public:
    ~Foo()
    {
        // When the following line is uncommented, the program CRASHES
        // when the destructor is exited - because the Obj destructors
        // are called on the invalid Obj ghost instances

        //boost::singleton_pool<boost::fast_pool_allocator_tag,
        //                  sizeof(std::pair<int const, Obj>)>::purge_memory();
    }

    fast_int_to_int_map mmap;
};

void mfoo()
{
    // When this function exits, the Foo instance goes off the stack
    // and its destructor is called, in turn calling the destructors
    // of the Obj instances - this is NOT desired!

    Foo foo;
    foo.mmap[0] = Obj();
    foo.mmap[1] = Obj();
}

int main()
{
    mfoo();

    // The following line deallocates the memory of the pool just fine -
    // but does nothing to prevent the destructors of the Obj instances
    // from being called

    boost::singleton_pool<boost::fast_pool_allocator_tag,
                          sizeof(std::pair<int const, Obj>)>::purge_memory();
}

As noted in the code comments, the destructors of the Obj instances which are managed by the boost::pool are always called.

What can I do to make the promising quote from the Boost Pool documention, drop them off into oblivion, come true?

Dan Nissenbaum
  • 13,558
  • 21
  • 105
  • 181

3 Answers3

3

You pass the custom allocator into your std::map class. So, this allocator will be used for everything inside of std::map: for all Obj data and also for nodes of a binary tree of std::map. As the result, if you call purge_memory() in Foo's destructor then all this memory becomes invalid, and it crashes in std::map destructor.

Your assumption that a custom allocator is responsible for objects' de-allocation is not correct: it's a std::map's task to free all objects. So, ~Obj() will be called regardless if you pass the custom allocator or if you use a default one.

I don't see any elegant solution for your question. But this approach should work:

  • use placement new to create Foo object from pool's memory,
  • use this Foo object as usual,
  • call purge_memory() to release all memory from the pool. No destructors ~Obj or ~std::map will be called.
qehgt
  • 2,972
  • 1
  • 22
  • 36
  • Thanks! I came to the same conclusion - in regards to the fact that the custom allocator is responsible for the *positioning* of memory, not for *calling the constructor or destructor*. It is now clear to me that these are two COMPLETELY separate processes. I.e., (1) The allocation/deallocation of memory itself using either system SDK memory calls, `new`/`delete`, or `malloc`/`free`; and (2) the calling of ctor's and dtor's. Custom allocators are **only** responsible for the former (#1), not the latter. – Dan Nissenbaum Apr 09 '14 at 01:14
  • However, a major part of my confusion arose from the fact that in the code above, **the function `purge_memory()` does nothing**. If it were a **`set`**, not a `map`, `purge_memory` would deallocate the memory. However, the **size** of the internal objects created by the `map` when the `map` (internally) calls `new` on the custom allocator is **not** the size of a pair. This is why `purge_memory()` does nothing, above. (In the case of a `set`, the size of the internal `new` request **is** the size of the contained objects, so it works in that case.) I am working on the latter problem now. – Dan Nissenbaum Apr 09 '14 at 01:20
1

By default pool uses the default_user_allocator_new_delete allocator. This will destroy underlying objects by calling the destructor first and then reclaiming underlying memory. The default_user_allocator_malloc_free will cause malloc'ed memory to be reclaimed without firing the destructor - hence drop[ing] them off into oblivion.

That said, if your tree is really that complicated, using free instead of firing destructors seems like a really good way to start chopping branches out from under yourself and potentially start leaking memory you can no longer reach.

Andy
  • 1,663
  • 10
  • 17
  • There is a single top-level object, and every data member that is a container utilizes the custom allocator, and so on through all nested objects, so that there is not a single path through the tree's hierarchy that is missed. I then simply let the single top-level object drop off the stack, and *its* destructor calls `purge` on all of the memory pools. Thanks so much for this answer - it is obvious in retrospect! Of course, that is the big difference between `malloc/free` and `new/delete` - the former does not call constructors and destructors. I never use `malloc/free` so didn't think of it. – Dan Nissenbaum Apr 08 '14 at 17:29
  • I am trying this now - do you know if the `boost::default_user_allocator_malloc_free` allocator *will* ensure that the **constructors** are called? – Dan Nissenbaum Apr 08 '14 at 17:31
  • That I don't know - in your example you called constructors explicitly. I presume you'd do something similar in your implementation. – Andy Apr 08 '14 at 17:34
  • An example would be useful - it seems my destructors are still always called, even with the use of `boost::default_user_allocator_malloc_free`, unfortunately. – Dan Nissenbaum Apr 08 '14 at 18:03
0

The answer to this question is contained in the comments beneath @qehgt's answer, and in detail in this new question.

Namely:

There is a clear and formal distinction between two related aspects of object instantiation and deletion:

  • (1) Allocation and deallocation of memory

  • (2) Calling of constructors and destructors

The purpose of a custom allocator is (1) only.

The container objects handle (2), and they call functions in the custom allocator to determine the location of memory in which to construct the object, and to tell the custom allocator that it's OK to free the allocated memory for given objects. But the calls to the constructor/destructor themselves are made by the container, not by the custom allocator.

Therefore, the way to achieve the goal of this question is the following: Declare the container object via new and NEVER CALL delete on it (but use the custom allocator to guarantee that the objects you later create in the container are stored in the memory pool, and then manually free the memory pool yourself by calling purge_memory()):

class Obj { // has operator<() for use with std::set };

typedef std::set<Obj, std::less<Obj>, boost::fast_pool_allocator<Obj>> fast_set_obj;

// Deliberately do not use a managed pointer -
// I will *NOT* delete this object, but instead
// I will manage the memory using the memory pool!!!
fast_set_obj * mset = new fast_set_obj;

// ... add some Obj's to 'mset'
mset->insert(Obj());
mset->insert(Obj());

// Do something desireable with the set ...
...

// All done.
// It's time to release the memory, but do NOT call any Obj destructors.
// The following line of code works exactly as intended.

boost::singleton_pool<boost::fast_pool_allocator_tag, sizeof(Obj const)>::purge_memory();

(The above code is taken from the linked question, above.)

However, I am still having an issue, not directly related to the intention behind this current question: The code above does not work if a map is used instead of a set. See the linked question for details.

Community
  • 1
  • 1
Dan Nissenbaum
  • 13,558
  • 21
  • 105
  • 181