Why would I std::move an std::shared_ptr?

69,159

Solution 1

I think that the one thing the other answers did not emphasize enough is the point of speed.

std::shared_ptr reference count is atomic. increasing or decreasing the reference count requires atomic increment or decrement. This is hundred times slower than non-atomic increment/decrement, not to mention that if we increment and decrement the same counter we wind up with the exact number, wasting a ton of time and resources in the process.

By moving the shared_ptr instead of copying it, we "steal" the atomic reference count and we nullify the other shared_ptr. "stealing" the reference count is not atomic, and it is hundred times faster than copying the shared_ptr (and causing atomic reference increment or decrement).

Do note that this technique is used purely for optimization. copying it (as you suggested) is just as fine functionality-wise.

Solution 2

By using move you avoid increasing, and then immediately decreasing, the number of shares. That might save you some expensive atomic operations on the use count.

Solution 3

Move operations (like move constructor) for std::shared_ptr are cheap, as they basically are "stealing pointers" (from source to destination; to be more precise, the whole state control block is "stolen" from source to destination, including the reference count information).

Instead copy operations on std::shared_ptr invoke atomic reference count increase (i.e. not just ++RefCount on an integer RefCount data member, but e.g. calling InterlockedIncrement on Windows), which is more expensive than just stealing pointers/state.

So, analyzing the ref count dynamics of this case in details:

// shared_ptr<CompilerInvocation> sp;
compilerInstance.setInvocation(sp);

If you pass sp by value and then take a copy inside the CompilerInstance::setInvocation method, you have:

  1. When entering the method, the shared_ptr parameter is copy constructed: ref count atomic increment.
  2. Inside the method's body, you copy the shared_ptr parameter into the data member: ref count atomic increment.
  3. When exiting the method, the shared_ptr parameter is destructed: ref count atomic decrement.

You have two atomic increments and one atomic decrement, for a total of three atomic operations.

Instead, if you pass the shared_ptr parameter by value and then std::move inside the method (as properly done in Clang's code), you have:

  1. When entering the method, the shared_ptr parameter is copy constructed: ref count atomic increment.
  2. Inside the method's body, you std::move the shared_ptr parameter into the data member: ref count does not change! You are just stealing pointers/state: no expensive atomic ref count operations are involved.
  3. When exiting the method, the shared_ptr parameter is destructed; but since you moved in step 2, there's nothing to destruct, as the shared_ptr parameter is not pointing to anything anymore. Again, no atomic decrement happens in this case.

Bottom line: in this case you get just one ref count atomic increment, i.e. just one atomic operation.
As you can see, this is much better than two atomic increments plus one atomic decrement (for a total of three atomic operations) for the copy case.

Solution 4

There are two reasons for using std::move in this situation. Most responses addressed the issue of speed, but ignored the important issue of showing the code's intent more clearly.

For a std::shared_ptr, std::move unambiguously denotes a transfer of ownership of the pointee, while a simple copy operation adds an additional owner. Of course, if the original owner subsequently relinquishes their ownership (such as by allowing their std::shared_ptr to be destroyed), then a transfer of ownership has been accomplished.

When you transfer ownership with std::move, it's obvious what is happening. If you use a normal copy, it isn't obvious that the intended operation is a transfer until you verify that the original owner immediately relinquishes ownership. As a bonus, a more efficient implementation is possible, since an atomic transfer of ownership can avoid the temporary state where the number of owners has increased by one (and the attendant changes in reference counts).

Solution 5

Copying a shared_ptr involves copying its internal state object pointer and changing the reference count. Moving it only involves swapping pointers to the internal reference counter, and the owned object, so it's faster.

Share:
69,159

Related videos on Youtube

sdgfsdh
Author by

sdgfsdh

fun times

Updated on July 21, 2022

Comments

  • sdgfsdh
    sdgfsdh almost 2 years

    I have been looking through the Clang source code and I found this snippet:

    void CompilerInstance::setInvocation(
        std::shared_ptr<CompilerInvocation> Value) {
      Invocation = std::move(Value);
    }
    

    Why would I want to std::move an std::shared_ptr?

    Is there any point transferring ownership on a shared resource?

    Why wouldn't I just do this instead?

    void CompilerInstance::setInvocation(
        std::shared_ptr<CompilerInvocation> Value) {
      Invocation = Value;
    }
    
  • Joseph Ireland
    Joseph Ireland over 7 years
    Also worth noting: why don't they just pass by const reference, and avoid the whole std::move stuff? Because pass-by-value also lets you pass in a raw pointer directly and there will just be one shared_ptr created.
  • Bruno Ferreira
    Bruno Ferreira over 7 years
    @JosephIreland Because you cannot move a const reference
  • ratchet freak
    ratchet freak over 7 years
    @JosephIreland because if you call it as compilerInstance.setInvocation(std::move(sp)); then there will be no increment. You can get the same behavior by adding a overload that takes a shared_ptr<>&& but why duplicate when you don't need to.
  • Joseph Ireland
    Joseph Ireland over 7 years
    @BrunoFerreira I was answering my own question. You wouldn't need to move it because it's a reference, just copy it. Still only one copy instead of two. The reason they don't do that is because it would unnecessarily copy newly constructed shared_ptrs, e.g. from setInvocation(new CompilerInvocation), or as ratchet mentioned, setInvocation(std::move(sp)). Sorry if my first comment was unclear, I actually posted it by accident, before I had finished writing, and I decided to just leave it
  • YSC
    YSC over 7 years
    Is it not premature optimization?
  • OrangeDog
    OrangeDog over 7 years
    @YSC not if whoever put it there actually tested it.
  • Angew is no longer proud of SO
    Angew is no longer proud of SO over 7 years
    @YSC Premature optimisation is evil if it makes code harder to read or maintain. This one does neither, at least IMO.
  • Lightness Races in Orbit
    Lightness Races in Orbit over 7 years
    Indeed. This is not a premature optimisation. It is instead the sensible way to write this function.
  • xaviersjs
    xaviersjs about 5 years
    Is it really hundred times faster? Do you have benchmarks for this?
  • Adisak
    Adisak almost 5 years
    @xaviersjs The assignment requires an atomic increment followed by an atomic decrement when Value goes out of scope. Atomic operations can take hundreds of clock cycles. So yes, it really is that much slower.
  • xaviersjs
    xaviersjs almost 5 years
    @Adisak that's the first I've heard the fetch and add operation (en.wikipedia.org/wiki/Fetch-and-add) could take hundreds of cycles more than a basic increment. Do you have a reference for that?
  • russianfool
    russianfool almost 5 years
    @xaviersjs : stackoverflow.com/a/16132551/4238087 With register operations being a few cycles, 100's (100-300) of cycles for atomic fits the bill. Although metrics are from 2013, this still seems to be true especially for multi-socket NUMA systems.
  • qweruiop
    qweruiop almost 5 years
    Exactly what I'm looking for. Surprised how other answers ignore this important semantic difference. the smart pointers is all about ownership.
  • user202729
    user202729 over 4 years
    I think that when there's no threading in the code, shared pointer won't have to use atomic operations (at least on GCC). (reddit.com/r/cpp/comments/9byrhy/how_fast_is_stdshared_ptr/‌​…)
  • Erik Aronesty
    Erik Aronesty about 4 years
    Sometimes you think there's no threading in your code... but then some darn library comes along and ruins it for u. Better to use const references and std::move... if it's clear and obvious that you can.... than rely on pointer reference counts.
  • Vlad
    Vlad over 3 years
    I think ownership is especially crucial in lambda notation. Shared ptr capture by reference may not contribute to its reference counter and after the code exits and ptr destroyed you would have lambda with dangling pointer.
  • ManuelSchneid3r
    ManuelSchneid3r over 2 years
    Made a small benchmark and found that its about three times faster. Cost of copy vs move std::shared_ptr
  • Abhinav
    Abhinav about 2 years
    @Adisak, If intent is to steal, isn't it better to have unique pointer as argument rather than having a shared pointer
  • Adisak
    Adisak about 2 years
    @Abhinav - no... you need to assign a shared pointer from a shared pointer to properly maintain the lifetime management of the object. You cannot assign a shared pointer to a unique pointer argument and then assign that to a shared pointer. Shared pointer and unique pointer are mutually exclusive methods in object lifetime management.
  • dqbydt
    dqbydt almost 2 years
    See herbsutter.com/2013/06/05/… for a discussion of perf implications of passing a shared_ptr by value.
  • dqbydt
    dqbydt almost 2 years
    Only if it is a move assignment (RHS is an rvalue), not if it is a copy assignment.