4 ms·
Why don't we just use shared_ptr most of the time? Is it really that inefficient?
by 90s_dev 1y ago
Why don't we just use shared_ptr most of the time? Is it really that inefficient?
- ajross 1y agoFor the same reason that you don't use Rc for everything in Rust. Putting all your heap management behind reference counted pointers isn't "that inefficient", no. But if you don't need the direct control over heap behavior, you shouldn't be using C++ (or Rust) in the first place. Languages with GC-managed runtimes (Java, C#, Go, Swift, et. al.) are actually significantly more performant for almost all heap-bound use cases, actually. Reference counting kinda sucks for typical code.
- pjmlp 1y agoManual reference counting to be more precise. When a GC-managed language uses reference counting as implementation algorith, the compiler might be able to optimize the reference counting to only occur when it is unavoidable or too costly to reason about, in a similar vein to bounds checking. When using library types for reference counting, there is no way for the compiler to implement such optimizations, unless the types are somehow tagged with compiler intrisics so that they could apply the same kind of optimizations.
- ninkendo 1y ago> When using library types for reference counting, there is no way for the compiler to implement such optimizations Aren’t refcounts in shared_ptr done as part of the copy constructor and destructor? It seems like vanilla copy elision would count as optimizing away the refcounts.
- pjmlp 1y agoCopy constructor, destructor, move constructor, copy constructor. All of them have to do counter booking, value count and accessors, also possibly handle weak_ptr booking. Plus how it actually works isn't part of ISO C++, so you're betting your luck on how a specific C++ is actually going to implement it, and possible issues when compiler versions change, same compiler or to another one.
- ninkendo 1y agoC++17 has copy elision as part of the standard now, so you can be guaranteed of the scenarios where C++ is required to outright skip the copy/move/destruction, leaving no code paths left for any refcounts. It’s not everything, there’s still plenty of optimizations on the table, but copy elision should eliminate a ton of refcount bookkeeping.
- pjmlp 1y agoIn some scenarios, there is a reason why there a few conference talks on the gotchas regarding that, especially when coupled with RVO.
- ninkendo 1y ago> Languages with GC-managed runtimes (Java, C#, Go, Swift, et. al.) Swift does not have a GC managed runtime in the same sense as the other languages you listed [0], it’s basically a bunch of refcounts inserted by the compiler, with similar performance characteristics to Arc in Rust or shared_ptr in C++. (For classes, at least. Structs are value types and stack-allocated.) [0] Yes, automatic refcounting is a form of garbage collection, but Java/C#/Go use tracing GC’s and not direct refcounting, whereas with Swift it’s more like the compiler is wrapping all objects in a shared_ptr for you, and so destruction is explicit and happens at exact points.
- oytis 1y agoWhy, we do. Whenever there is data on heap that needs to be shared that is
- znkr 1y agoIt’s mostly fine, until you run into memory leaks due to circles or because some part of your program holds onto the root of some large shared pointer graph and you have no idea which part. If you take it very far, like some code bases I worked with did, you discover that everything needs to be shared pointer now, because most lifetimes are no longer explicit but implicitly defined by the life time of the shared pointer that holds it.
- flohofwoe 1y agoBecause shared_ptr (and also unique_ptr) nudges you towards keeping each object in its own heap allocation, and that quickly gets inefficient with large number of objects. E.g. a handful of large objects managed through shared_ptr is usually fine, but managing many tiny C++ object individually through smart pointers usually isn't. Instead store large groups of objects of the same type and similar lifetime by value in a std::vector (and then maybe put that std::vector behind a smart pointer). Also if you lean in too much on shared/unique pointers for large amounts of tiny objects you'll most likely end up in a situation where Java-style garbage collection would be more efficient. E.g. manual or semi-manual memory management is mostly about controlling the overall memory layout of your application's data to improve throughput, reducing the number of individual heap allocations is just a useful side effect.
- bluGill 1y agoMost of the time unique-ptr is faster and makes it easier to reason about your program. There is more than heap managemant to performant code and shared ptr makes it harder to reason about those other areas.
- fh973 1y agoYes, it is very inefficient, as it is thread-safe and uses atomic operations internally. So instead of just accessing memory, you have the cost of cross-cache coherency operations between CPU cores.
- William_BB 1y agoI take the following approach: - Stack by default - Unique ptr if needed on the heap - Shared ptr if needed to share ownership Although unique ptr is zero cost after make_unique(), I avoid polluting my heap unnecessarily. I've never benchmarked this though (keeping various objects on stack vs heap as unique ptr and how that impacts memory accesses) I'm quite junior. Appreciate anyone pointing out if anything I said doesn't make sense.
- green7ea 1y agoAuthor here, that's a good approach :-). I see shared_ptr as a code smell since shared ownership makes life difficult.
- grues-dinner 1y ago> Stack by default - Unique ptr if needed on the heap - Shared ptr if needed to share ownership Sounds about right. Shared ownership is fairly rare though, and you often only need shared access (reference/pointer if nullable) and can provide other, more explicit, ways of managing the lifetime. > unique ptr is zero cost after make_unique() Kind of, but compared to the stack, it could cause caching inefficiency because your heap-allocated thing could be almost anywhere, but your stack-allocated thing is probably in the cache already.
- grumbel 1y agoWith shared_ptr you completely lose track who actually owns an object or when it will get destructed. It encourages sloppy programming and makes code much harder to read and reason about. Unlike most languages that have ref-counting build in, shared_ptr also doesn't provide anything to deal with cyclic dependencies, so you can end up with memory leaks. The most important reason however is simply that you don't need it like 99% of the time, unique_ptr provides enough functionality to work just fine as a shared_ptr replacement in most situations. And in the rare cases where you really need a shared_ptr, you can just convert a unique_ptr into one.
- green7ea 1y agoAuthor here, parent comment describes it very well — shared_ptr are a last resort, not a first one. They are quite heavily (and badly) used in some code bases (ROS). I'm planning a future article that covers shared_ptr in more details. The surprising thing about shared pointer is that a `const shared_ptr<T>` means that you can modify the contents of T. This makes the problem, mentioned in the parent, of keeping track of who can modify the object where impossible. I've never encountered a `const shared_ptr<const T>` but that would be a better approach.
- rjinman 1y agounique_ptr is much better because then each object has a sole owner, which makes object lifetimes much easier to reason about and you can't end up with cyclic references causing memory leaks.
- majoe 1y agoReasons I can think of: - Wrapping all objects in shared pointer is annoying. - If you stick to that convention, you have to do it on every call side, while you only have to implement RAII once. - You can enforce invariants of your class with RAII, that you can't with a plain shared_ptr - Regarding efficiency: It has the overhead of reference counting plus you have to store all objects on the heap instead of the stack. In hot loops this may hurt cache locality.