0

Consider some source of data, that holds a shared_ptr (e.g. a struct member). If you have a guarantee that it is not a temporary value, but will be valid through the current scope and you want have an alias to that pointer: what is more performant, copying the shared_ptr or having a (possibly const) reference to the original?

Example:

struct S {
  shared_ptr<T> ptr;
};

void fun(S s) {
  shared_ptr<T> alias1 = s.ptr;
  shared_ptr<T> const& alias2 = s.ptr;
  /* do something with the alias */
}

Edit: Motivation. This might be necessary, when e.g. 1) getting s.ptr involves traversing a chain of function calls or derefs or 2) wanting to improve readability.

Tradeoff. Copying the pointer feels more "right" in the sense that it mimics what you would do with a raw pointer, but it requires the refcount mechanism to do its thing (on construction and destruction). On the other hand, having a reference that you potentially use often, incurs additional dereferencing which is in turn expensive (the dereferencing is on top of anything that you would do with the held T object).

Is there a common rule-of-thumb what is more performant?

bitmask
  • 32,434
  • 14
  • 99
  • 159
  • Comments are not for extended discussion; this conversation has been [moved to chat](https://chat.stackoverflow.com/rooms/192399/discussion-on-question-by-bitmask-copy-or-constref-a-shared-ptr). – Samuel Liew Apr 26 '19 at 00:23

2 Answers2

3

A rule of thumb

Algorithms that modify or manipulate data without taking on ownership should act on iterators or references to the data (or it's container).

For example, if you want to find the average of a bunch of values, you should write a function that takes iterators to the set of values (or takes the container by const reference). What you shouldn't do is pass a vector by copy, or pass a smart-pointer.

There's overhead associated with copying a shared_ptr, and algorithms themselves should always expect the data they manipulate to outlive the computation.

When to use shared_ptr? shared_ptr should be used when writing containers or other classes that need to hold on to a piece of data for an unknown amount of time. Objects take ownership, not functions, and not algorithms. (Member functions can, of course, accept a shared_ptr as an input, but it only makes sense to do so if the class they're apart of needs to own that data).

What about threads?

Threads are the exception to this rule of thumb. If you're starting a new thread, you should definitely pass any shared_ptrs by value. The thread may live for an unknown amount of time, and you need to ensure that whatever data it has access to remains valid until the thread exits.

Copying a shared_ptr is a lot cheaper than starting a thread anyways, so it's overhead is minimal.

Alecto Irene Perez
  • 10,321
  • 23
  • 46
  • References and pointers have overhead as well. The point of the whole question was to weigh the overhead of references (deref on each access) against the overhead of `shared_ptr`s (a couple branches and integer ops, but no overhead when accessing the object). – bitmask Apr 25 '19 at 22:41
  • When using `shared_ptr` you still have to dereference the object to access it. Internally, a reference is identical to a regular pointer, and the dereference only occurs when you read or modify the state of the value being referenced. This is true for `shared_ptr`s too. The only time you *don't* have overhead from dereferencing an object is when it's a value that the compiler decided to store in the CPU registers. – Alecto Irene Perez Apr 25 '19 at 22:44
  • No. Count the number of dereferences. `shared_ptr` has one. `shared_ptr&` has two. – bitmask Apr 25 '19 at 22:45
  • 1
    I meant that you should pass a reference or raw pointer to the data itself; not to the `shared_ptr` – Alecto Irene Perez Apr 25 '19 at 22:46
2

Sr. Perez is correct about the best way to use shared pointers.

If they are used at all, they should be used wisely as incrementing the reference count is a very expensive business on most compilers.

On one high-profile game I worked on, much of the frame time was occupied with one shared pointer dereference.

In the example bitmask gives, the structure itself is passed by value, which bizzarely is not as bad as it sounds because of copy-elision or similar optimisations.

What are copy elision and return value optimization?

If you have the misfortune of having a shared pointer to work with, you can turn it into a regular pointer with get().

Of course, this makes the code less safe, but who wants a boring life :)

#include <memory>

struct S {
  std::shared_ptr<int> ptr;
};

void fun(S s) {
    int *alias = s.ptr.get();
    *alias = 1;
}

Godbolt:

fun(S):                               # @fun(S)
    mov     rax, qword ptr [rdi]
    mov     dword ptr [rax], 1
    ret
Andy Thomason
  • 328
  • 1
  • 9