Dealing with std::string/std::vector member variables while using boost::singleton_pool
Asked Answered
U

2

9

I am writing a performance critical application in which I am creating large number of objects of similar type to place orders. I am using boost::singleton_pool for allocating memory. Finally my class looks like this.

    class MyOrder{
    std::vector<int> v1_;
    std::vector<double> v2_;

    std::string s1_;
    std::string s2_;

public:
    MyOrder(const std::string &s1, const std::string &s2): s1_(s1), s2_(s2) {}

    ~MyOrder(){}

    static void * operator new(size_t size); 
    static void operator delete(void * rawMemory) throw();
    static void operator delete(void * rawMemory, std::size_t size) throw();

};

struct MyOrderTag{};
typedef boost::singleton_pool<MyOrderTag, sizeof(MyOrder)> MyOrderPool; 

void* MyOrder:: operator new(size_t size)
{
    if (size != sizeof(MyOrder)) 
        return ::operator new(size);

    while(true){
        void * ptr = MyOrderPool::malloc();
        if (ptr != NULL) return ptr;

        std::new_handler globalNewHandler = std::set_new_handler(0);
        std::set_new_handler(globalNewHandler);

        if(globalNewHandler)  globalNewHandler();
        else throw std::bad_alloc();

    }
}

void MyOrder::operator delete(void * rawMemory) throw()
{
    if(rawMemory == 0) return; 
    MyOrderPool::free(rawMemory);
}

void MyOrder::operator delete(void * rawMemory, std::size_t size) throw()
{
    if(rawMemory == 0) return;
    if(size != sizeof(Order)) {
        ::operator delete(rawMemory);
    }
    MyOrderPool::free(rawMemory);
}

I recently posted a question about performance benefit in using boost::singleton_pool. When I compared the performances of boost::singleton_pool and default allocator, I did not gain any performance benefit. When someone pointed that my class had members of the type std::string, whose allocation was not being governed by my custom allocator, I removed the std::string variables and reran the tests. This time I noticed a considerable performance boost.

  1. Now, in my actual application, I cannot get rid of member variables of time std::string and std::vector. Should I be using boost::pool_allocator with my std::string and std::vector member variables?

  2. boost::pool_allocator allocates memory from an underlying std::singleton_pool. Will it matter if different member variables (I have more than one std::string/std::vector types in my MyOrder class. Also I am employing pools for classes other than MyOrder which contain std::string/std::vector types as members too) use the same memory pool? If it does, how do I make sure that they do one way or the other?

Unbend answered 8/5, 2012 at 14:27 Comment(3)
Use const & in your constructor.Hornwort
@EternalLearner Good Point. That will prevent one copy. Done.Unbend
wouldn't it be a bit more appropriate to actually use std::weak_ptr's or something to the std::strings here? taking references for private internal copies always just feels wrong to me. even if you defer the copy...Fireweed
K
2
  1. Now, in my actual application, I cannot get rid of member variables of time std::string and std::vector. Should I be using boost::pool_allocator with my std::string and std::vector member variables?

I have never looked into that part of boost, but if you want to change where strings allocate their memory, you need to pass a different allocator to std::basic_string<> at compile time. There is no other way. However, you need to be aware of the downsides of that: For example, such strings will not be assignable to std::string anymore. (Although employing c_str() would work, it might impose a small performance penalty.)

  1. boost::pool_allocator allocates memory from an underlying std::singleton_pool. Will it matter if different member variables (I have more than one std::string/std::vector types in my MyOrder class. Also I am employing pools for classes other than MyOrder which contain std::string/std::vector types as members too) use the same memory pool? If it does, how do I make sure that they do one way or the other?

The whole point of a pool is to put more than one object into it. If it was just one, you wouldn't need a pool. So, yes, you can put several objects into it, including the dynamic memory of several std::string objects.

Whether this gets you any performance gains, however, remains to be seen. You use a pool because you have reasons to assume that it is faster than the general-purpose allocator (rather than using it to, e.g., allocate memory from a specific area, like shared memory). Usually such a pool is faster because it can make assumptions on the size of the objects allocated within. That's certainly true for your MyOrder class: objects of it always have the same size, otherwise (larger derived classes) you won't allocate them in the pool.
That's different for std::string. The whole point of using a dynamically allocating string class is that it adapts to any string lengths. The memory chunks needed for that are of different size (otherwise you could just char arrays instead). I see little room for a pool allocator to improve over the general-purpose allocator for that.


On a side note: Your overloaded operator new() returns the result of invoking the global one, but your operator delete just passes anything coming its way to that pool's free(). That seems very suspicious to me.

Kwok answered 8/5, 2012 at 19:43 Comment(8)
I elaborated more on "different variables" in my question.Unbend
I am following the exact procedure described in Effective C++ (Third Edition) items 49 and 51 (my.safaribooksonline.com/0321334876)Unbend
@sank: I still haven't found the time to get hold of a copy of the book and look into it, but I fired a link to this towards Scott Meyers, and he wrote to me that (I am re-translating this from German) "there's two versions of operator delete on page 255. The first is the non-member version, and it's the one he copied. The second version is the member version and that is the one he should have copied. It contains the test you supposedly expected: [...] if (size != sizeof(Base)) [...]". So you might want to go back and read this more thoroughly. HTH!Kwok
FWIW, I now have the book in front of me and can confirm what Scott wrote.Kwok
I saw the other version of operator delete and have it in my code but I cannot think of a situation when that will be called.Unbend
@sank: The member version of operator delete will be called whenever an object of that class will be deleted.Kwok
I know that, but I cannot think of a condition when operator delete(void * rawMemory, size_t size) will be called. I have it in my code(not displayed here) though.Unbend
@sank: When you overload operator delete(), you can decide whether you want the runtime system to pass a size argument or not by adding such an argument or not to the operator function. If you add it, the runtime system will pass that argument. Always. There's nothing you need to do to get it except for invoking delete on some pointer expression. It's usually handy for class-specific overloading of operator delete().Kwok
G
1

Using a custom allocator for the std::string/std::vector in your class would work (assuming the allocator is correct) - but only performance testing will see if you really see any benefits from it.

Alternatively, if you know that the std::string/std::vector will have upper limits, you could implement a thin wrapper around a std::array (or normal array if you don't have c++11) that makes it a drop in replacement.

Even if the size is unbounded, if there is some size that most values would be less than, you could extend the std::array based implementations above to be expandable by allocating with your pooled allocator if they fill up.

Glaive answered 17/5, 2012 at 18:54 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.