问题
I have following data model
struct Base {
int x_;
int y_;
int z_;
virtual int getId() const;
virtual int getValue() const = 0;
virtual Base* create() const = 0;
bool operator<(const Base &other);
};
struct Derived0 : public Base {
virtual Derived0* create() const { return new Derived0(); };
virtual int getId() const;
virtual int getValue() const;
};
//...
struct DerivedN : public Base {
virtual DerivedN* create() const { return new DerivedN(); };
virtual int getId() const;
virtual int getValue() const;
};
and fill it in following way (simplified)
int n = 0;
std::shared_ptr<Base> templ[100];
templ[n++] = std::make_shared<Derived0>();
//...
templ[n++] = std::make_shared<DerivedN>();
std::vector<std::shared_ptr<Base>> b;
for (int i = 0; i < n; i++) {
while (...) { // hundreds of thousands iterations
std::shared_ptr<Base> ptr(templ[i]->create());
// previous call consumes most of the time
//...
b.push_back(ptr);
}
}
std::sort(b.begin(), b.end());
// ...
Since I need huge amount of derived objects I wonder if the initialization can be done more efficiently. In showed case most of the time is spend by creating single shared pointers.
I tried a way with preallocating an array of Base objects (since all Derived have same size), casting virtual type for each template and storing raw pointers to this array. Not surprisingly such approach is many times faster.
However is not clean, vector cannot be used and memory management is problematic.
Can somebody give me an advice, how to do this efficiently in C++ way
- if all objects have same size?
- if the size varies?
回答1:
It seems to me that a lot of your performance issues could be solved by using std::unique_ptr and reserving some std::vector memory in advance.
std::shared_ptr<Base> ptr(templ[i]->create());
The above line involves dynamically allocating memory for both the derived type and the std::shared_ptr control block. If you don't have shared ownership semantics, then using std::unique_ptr instead will eliminate the need for one of those allocations.
b.push_back(ptr);
When you do the above enough times, the vector will run out of memory it has allocated for you and try and allocate some more. std::vector is designed in such a way that this has amortized constant time complexity, but anything we can do to mitigate that, especially with huge vectors, will save time.
Your new code might look something like:
std::vector<std::unique_ptr<Base>> b;
b.reserve(n * /*number of iterations*/);
for (int i = 0; i < n; i++) {
while (...) { // hundreds of thousands iterations
std::unique_ptr<Base> ptr(templ[i]->create());
//...
b.push_back(ptr);
}
}
As an aside, you could limit code duplication for your prototype array creation by doing something like this:
template <class Base, class... Derived, std::size_t... Idx>
auto arrayOfUniqueDerived (std::index_sequence<Idx...>)
{
std::array<std::unique_ptr<Base>, sizeof...(Derived)> arr;
(void) std::initializer_list<int> { (arr[Idx] = std::make_unique<Derived>(), 0)... };
return arr;
}
template <class Base, class... Derived>
auto arrayOfUniqueDerived ()
{
return arrayOfUniqueDerived<Base,Derived...>(std::index_sequence_for<Derived...>{});
}
Then use it like:
std::array<std::unique_ptr<Base>,3> templ =
arrayOfUniqueDerived<Base,Derived0,Derived1,Derived2>();
回答2:
Create a variant style type eraser that makes everything look like Base:
template<class T>struct tag{using type=T;};
template<class Base, class...Derived>
struct poly {
Base* get(){
return const_cast<Base*>( const_cast<poly const*>( this )->get() );
}
Base const* get()const{
if (!ops) return nullptr;
return ops->to_base(&raw);
}
Base* operator->(){ return get(); }
Base const* operator->()const{ return get(); }
Base& operator*(){ return *get(); }
Base const& operator*()const{ return *get(); }
explicit operator bool()const{ return get(); }
template<class T,class...Args,
class=std::enable_if<
/* T is one of Derived... */
>
>
void emplace(tag<T>,Args&&...args){
cleanup();
ops=&ops_for<T>();
new(&raw)T(std::forward<Args>(args)...);
}
poly& operator=(poly const& o){
if (this==&o)return *this;
cleanup();
if (!o->ops) return *this;
o->ops.copy_ctor( &raw, &o.raw );
ops=o->ops;
return *this;
}
poly& operator=(poly&&o){
if (this==&o)return *this;
cleanup();
if (!o->ops) return *this;
o->ops.move_ctor( &raw, &o.raw );
ops=o->ops;
return *this;
}
poly(poly const& o){
if (!o->ops)return;
o->ops.copy_ctor(&raw,&o.raw);
ops=o->ops;
}
poly(poly&& o){
if (!o->ops)return;
o->ops.move_ctor(&raw,&o.raw);
ops=o->ops;
}
private:
void cleanup(){
if (ops) ops->dtor(&raw);
ops=nullptr;
}
struct erase_ops{
void(*copy_ctor)(void*lhs,void const*rhs);
void(*move_ctor)(void*lhs,void*rhs);
void(*dtor)(void*ptr);
Base const*(*to_base)(void const*ptr);
};
template<class D>
static erase_ops const& ops_for(){
static erase_ops r={
// ...
};
return r;
};
erase_ops const* ops=nullptr; // = &ops_for<Derived1>(); etc
std::aligned_storage< /* size and alignment info */ > raw;
};
implementation left out, am on phone.
Once you have above, you can create a vector of poly<Base, Derived1, Derived2, ..... The cost is one extra pointer per instance.
Now at this point we have already duplicayed most of virtual dispatch, so we could just include in the type erase the remaining operations on DerivedN that are implemented as virtual methods and shave off another pointer's cost. If Base is modestly bigger, I would not bother.
C++ loves value types. Give it what it wants.
来源:https://stackoverflow.com/questions/30933001/how-to-efficiently-allocate-space-for-vector-of-pointers-to-virtual-base-class-i