Search code examples
c++performanceshared-ptrreference-countingweak-ptr

C++ weak_ptr creation performance


I've read that creating or copying a std::shared_ptr involves some overhead (atomic increment of reference counter etc..).

But what about creating a std::weak_ptr from it instead:

Obj * obj = new Obj();
// fast
Obj * o = obj;
// slow
std::shared_ptr<Obj> a(o);
// slow
std::shared_ptr<Obj> b(a);
// slow ?
std::weak_ptr<Obj> c(b);

I was hoping in some faster performance, but i know that the shared pointer still have to increment the weak references counter.. So is this still as slow as copying a shared_ptr into another?


Solution

  • In addition to Alec's very interesting description of the shared/weak_ptr system used in his previous projects, I wanted to give a little more detail on what is likely to be happening for a typical std::shared_ptr/weak_ptr implementation:

    // slow
    std::shared_ptr<Obj> a(o);
    

    The main expense in the above construction is to allocate a block of memory to hold the two reference counts. No atomic operations need be done here (aside from what the implementation may or may not do under operator new).

    // slow
    std::shared_ptr<Obj> b(a);
    

    The main expense in the copy construction is typically a single atomic increment.

    // slow ?
    std::weak_ptr<Obj> c(b);
    

    The main expense in the this weak_ptr constructor is typically a single atomic increment. I would expect the performance of this constructor to be nearly identical to that of the shared_ptr copy constructor.

    Two other important constructors to be aware of are:

    std::shared_ptr<Obj> d(std::move(a));  // shared_ptr(shared_ptr&&);
    std::weak_ptr<Obj> e(std::move( c ));  // weak_ptr(weak_ptr&&);
    

    (And matching move assignment operators as well)

    The move constructors do not require any atomic operations at all. They just copy the reference count from the rhs to the lhs, and make the rhs == nullptr.

    The move assignment operators require an atomic decrement only if the lhs != nullptr prior to the assignment. The bulk of the time (e.g. within a vector<shared_ptr<T>>) the lhs == nullptr prior to a move assignment, and so there are no atomic operations at all.

    The latter (the weak_ptr move members) are not actually C++11, but are being handled by LWG 2315. However I would expect it to already be implemented by most implementations (I know it is already implemented in libc++).

    These move members will be used when scooting smart pointers around in containers, e.g. under vector<shared_ptr<T>>::insert/erase, and can have a measurable positive impact compared to the use of the smart pointer copy members.

    I point it out so that you will know that if you have the opportunity to move instead of copy a shared_ptr/weak_ptr, it is worth the trouble to type the few extra characters to do so.