-2

I have been using boost object_pool for some time and was generally happy with the results. Previously I was mostly allocating individual objects but rarely freed them individually, just freed entire pool at once. Recently when I came across the need to free many objects from the pool I discovered that it is VERY slow.

Apparently pool is searching through the list of already released chunks to link the newly released object. The documentation talks about ordered and unordered pools and mentions pool_allocator as well as fast_pool_allocator. Unordered pool (using fast_memory_allocator) presumably would be much faster in releasing memory chunks. However, I can't see how I can make use of it.

Do I understand correctly that I have a choice between pool_allocator and fast_pool_allocator only in conjunction with boost::singleton_pool but not with boost::object_pool?

Below is a small test program illustrating the problem. It was built with VS2013 and boost 1_57_0. Test allocates n objects in object pool and then randomly releases 10%. It has some crude timing instrumentation which shows that for n == 100,000 allocation takes 0.004 sec while release takes 0.4 sec. Meanwhile for n == 1,000,000 it takes 0.022 sec to allocate and 42 sec to release on my machine.

#include <boost/pool/object_pool.hpp>
#include "time.h"
#include <vector>
#include <random>

struct foo {
    int data[10];
};

struct test {
    test(unsigned n) : size{ n } {}
    void run();
    float elapsedSec(clock_t& watch);
    unsigned size;
    boost::object_pool<foo> _pool;
    float mallocSec;
    float freeSec;
};

void test::run() {
    std::vector<foo *> foos(size, nullptr);
    std::default_random_engine generator;
    std::uniform_int_distribution<int> distribution(0, size - 1);
    auto dice = std::bind(distribution, generator);
    clock_t watch = clock();
    for (int i = 0; i < size; ++i)
        foos[i] = _pool.malloc();
    mallocSec = elapsedSec(watch);
    for (int i = 0; i < size / 10;) {
        auto idx = dice();
        if (foos[idx] == nullptr)
            continue;
        _pool.free(foos[idx]);
        foos[idx] = nullptr;
        i += 1;
    }
    freeSec = elapsedSec(watch);
}

float test::elapsedSec(clock_t& watch) {    
    clock_t start = watch;
    watch = clock();
    return (watch - start) / static_cast<float>(CLOCKS_PER_SEC);   
}
LRaiz
  • 667
  • 7
  • 18
  • Show us where you are stuck. We need to have the code that compiles except for what you are trying to change. Because we sure are not going to read all the docs, create a sample application, fix it so it does what we understood you want, only to find out that your question was actually slightly different. http://stackoverflow.com/help/how-to-ask – sehe Apr 21 '16 at 07:05
  • I object to moderator taking points from my reputation. I am not stuck, I came across a known limitation of boost object pools and was asking if experts knew of a workaround. If one googles, he would find multiple links confirming my assertions (including some on stack overflow). I did not want to distract readers attention by referencing them. Any small program that allocates several million objects from boost::object_pool and then tries to free a few hundred thousand would demonstrate a severe performance problem. I expect it to be a fact known to experts in this field. – LRaiz Apr 21 '16 at 15:05
  • 1. we are not moderators, just peers trying to answer :) 2. I was the one asking for code. 3. I didn't "take points" (downvote). I might, if the question cannot be improved :) – sehe Apr 21 '16 at 15:26
  • I think the question is clear for people familiar with boost pools. I edited the original post to bold it. Instead of threatening, please explain what is not clear. – LRaiz Apr 21 '16 at 15:50
  • Note that i left my constructive comment hours before your defensive rant – sehe Apr 21 '16 at 16:06
  • There is nothing constructive in your comment. I am not trying to change boost pools, just trying to use them and looking for a workaround for a known issue. If you aren't qualified to answer my question then just stop wasting my time. – LRaiz Apr 21 '16 at 16:26
  • 2
    Good job driving away someone who tried to help (and from what I know is also qualified to help). – R. Martinho Fernandes Apr 21 '16 at 16:41
  • Trying to charm your helpers into helpfulness? I happen to be aware of Boost Pool. I find the various pools & allocators slightly confusing (prefer Interprocess). I'm simply not inclined to try and check whether I can arrive at a sample that unambiguously matches your prose description. Aside from that, I think I know a solution that should be fast, but it's pretty prominent in the docs, so I fear you already know it and will dismiss it for some valid reason. I'll bow out and hope someone from your apparent [target audience](http://stackoverflow.com/tags/boost/topusers) to pick up. Good Luck! – sehe Apr 21 '16 at 16:44
  • I edited the original question to include sample program that illustrates the issue. – LRaiz Apr 22 '16 at 16:08
  • Downvoting purely for your abhorrent attitude above. Treating this community's experts in such a way is _not_ the way to get them to solve _your_ problems for you for free. – Lightness Races in Orbit Apr 23 '16 at 13:26
  • I posted a reasonable question but receive an immediate downvote accompanied by a comment that is not truly applicable. The comment also suggested something that looked like a busywork to me. When I objected to a downvote being unreasonable I got additional downvotes from the brotherhood of the regulars. Then I amend the post with a complete code to reproduce the issue and got rewarded by an additional downvote and a name-calling. Still not a single response related to the essence of the question. Interesting pattern. – LRaiz Apr 24 '16 at 12:57
  • That's an excellent sample. I'm miffed by the performance characteristics. Looking at it closer now. – sehe Apr 24 '16 at 23:01
  • Thanks. BTW, my original post/question is probably imprecise. The way I understand things now pool_allocator and fast_pool_allocations are classes that stand on their own and are not related to arguments/options of singleton_pool. – LRaiz Apr 24 '16 at 23:56
  • Okay. Answered. Remember when I said I find the Boost Pool offering not so nice (and I prefer Boost Interprocess's take on allocators)? The answer re-confirms this. Stateful allocators are a good match here and pretty well supported in c++11 onwards. Not all standard library implementations have good `scoped_allocator` adaptors yet, but boost does. – sehe Apr 25 '16 at 01:06

1 Answers1

5

The free call hits ordered_free on the underlying allocator. Indeed, ordered_malloc_need_resize() takes basically all the execution time.

Do I understand correctly that I have a choice between pool_allocator and fast_pool_allocator only in conjunction with boost::singleton_pool but not with boost::object_pool?

Yes. The object_pool manifestly uses the ordered_free function on the underlying simple_segregated_storage. This is clearly by design (although the rationale escapes me for the moment. Apparently in the intended usage it makes sense for object_pool to always optimize for array allocations/de-allocations).

The way I understand things now pool_allocator and fast_pool_allocations are classes that stand on their own and are not related to arguments/options of singleton_pool.

Yes. They are hardwired to use the singleton pools instances. Boost Pool clearly predates standard library support for stateful allocators. You could copy the implementation of fast_pool_allocator out to use a runtime instance of pool instead of the singleton pool.

The following sample makes non_boost::fast_pool_allocator a stateful allocator on top of a specific "Object Use" pool instance. That makes tha allocator stateful. The state is mainly a pointer to the pool.

_alloc.destroy is used to destruct the foo instance and free the memory. Any unfreed elements will still be freed upon destruction of the _pool (Note since we don't use object_pool, no destructors for foo will be run in such a case. In your sample, foo is POS and therefore trivially destructible. If not, you can of course use a std::unique_ptr or similar, or indeed write a version of object_pool that doesn't insist on ordered allocation).

DEMO

Live On Coliru

#include <boost/pool/pool.hpp>
#include <boost/pool/object_pool.hpp>
#include <boost/pool/pool_alloc.hpp>
#include "time.h"
#include <vector>
#include <random>

struct foo {
    int data[10];
};

namespace non_boost {
    template <typename T, typename UserAllocator = boost::default_user_allocator_new_delete>
    class fast_pool_allocator
    {
      public:
        typedef T value_type;
        typedef UserAllocator user_allocator;

        typedef value_type * pointer;
        typedef const value_type * const_pointer;
        typedef value_type & reference;
        typedef const value_type & const_reference;
        typedef boost::pool<UserAllocator> pool_type;
        typedef typename pool_type::size_type       size_type;
        typedef typename pool_type::difference_type difference_type;

        template <typename U>
        struct rebind {
            typedef fast_pool_allocator<U, UserAllocator> other;
        };

        pool_type* _ref;
      public:
        fast_pool_allocator(pool_type& ref) : _ref(&ref) { }

        fast_pool_allocator(fast_pool_allocator const&) = default;
        fast_pool_allocator& operator=(fast_pool_allocator const&) = default;

        // Not explicit, mimicking std::allocator [20.4.1]
        template <typename U>
        fast_pool_allocator(const fast_pool_allocator<U, UserAllocator> & other) : _ref(other._ref)
        { }

        // Default destructor used.
        static pointer address(reference r)                     { return &r;                                      } 
        static const_pointer address(const_reference s)         { return &s;                                      } 
        static size_type max_size()                             { return (std::numeric_limits<size_type>::max)(); } 
        void construct(const pointer ptr, const value_type & t) { new (ptr) T(t);                                 } 
        void destroy(const pointer ptr)                         { ptr->~T();                                      } 

        bool operator==(fast_pool_allocator const& rhs) const { return _ref == rhs._ref; }
        bool operator!=(fast_pool_allocator const& rhs) const { return _ref != rhs._ref; }

        pointer allocate(const size_type n)
        {
            const pointer ret = (n == 1) 
                ? static_cast<pointer>( (_ref->malloc)() ) 
                : static_cast<pointer>( _ref->ordered_malloc(n) );
            if (ret == 0)
                boost::throw_exception(std::bad_alloc());
            return ret;
        }

        pointer allocate(const size_type n, const void * const) { return allocate(n); }
        pointer allocate()
        {
            const pointer ret = static_cast<pointer>( (_ref->malloc)() );
            if (ret == 0)
                boost::throw_exception(std::bad_alloc());
            return ret;
        }
        void deallocate(const pointer ptr, const size_type n)
        {

#ifdef BOOST_NO_PROPER_STL_DEALLOCATE
            if (ptr == 0 || n == 0)
                return;
#endif
            if (n == 1)
                (_ref->free)(ptr);
            else
                (_ref->free)(ptr, n);
        }
        void deallocate(const pointer ptr) { (_ref->free)(ptr); }
    };

    //Specialization of fast_pool_allocator<void> required to make the allocator standard-conforming.
    template<typename UserAllocator>
    class fast_pool_allocator<void, UserAllocator> {
    public:
        typedef void*       pointer;
        typedef const void* const_pointer;
        typedef void        value_type;

        template <class U> struct rebind {
            typedef fast_pool_allocator<U, UserAllocator> other;
        };
    };

}

struct test {
    test(unsigned n) : size{ n } {}
    void run();
    float elapsedSec(clock_t& watch);
    unsigned size;

    boost::pool<boost::default_user_allocator_malloc_free> _pool { sizeof(foo) };
    non_boost::fast_pool_allocator<foo, boost::default_user_allocator_malloc_free> _alloc { _pool };

    float mallocSec;
    float freeSec;
};

void test::run() {
    std::vector<foo *> foos(size, nullptr);
    std::default_random_engine generator;
    std::uniform_int_distribution<int> distribution(0, size - 1);

    auto dice = std::bind(distribution, generator);
    clock_t watch = clock();

    for (unsigned i = 0; i < size; ++i)
         foos[i] = _alloc.allocate();

    mallocSec = elapsedSec(watch);

    for (unsigned i = 0; i < size / 10;) {
        auto idx = dice();
        if (foos[idx] != nullptr)
        {
            _alloc.destroy(foos[idx]);
            foos[idx] = nullptr;
        }
        i += 1;
    }

    freeSec = elapsedSec(watch);
}

float test::elapsedSec(clock_t& watch) {    
    clock_t start = watch;
    watch = clock();
    return (watch - start) / static_cast<float>(CLOCKS_PER_SEC);   
}

int main() {
    test t(10u << 20);
    t.run();

    std::cout << t.mallocSec << "\n";
    std::cout << t.freeSec   << "\n";
}

Prints (on my system):

0.135127
0.050991
sehe
  • 374,641
  • 47
  • 450
  • 633
  • 1
    Thanks for the effort and detailed recipe. I didn't really expect anyone to go through troubles of implementing alternative pools. The answer goes well beyond a short verbal response that I expected. It is much appreciated. – LRaiz Apr 25 '16 at 15:44
  • 1
    Oh look: It seems I'm forgetful, I had already [discovered most of this in March 2015](http://stackoverflow.com/questions/28925143/is-there-a-boost-pool-fixed-sized-allocator/28933392#comment46140912_28933392)... – sehe May 11 '16 at 16:18