4 ms·
Fully manually managed memory is definitely antiquated / obsolete. As others have mentioned, you really shouldn't even be doing new/delete in C++ anymore, but i
by kllrnohj 4y ago
Fully manually managed memory is definitely antiquated / obsolete. As others have mentioned, you really shouldn't even be doing new/delete in C++ anymore, but instead std::make_unique or std::make_shared. At which point it's mostly automatic memory management.
HOWEVER, partial manually managed memory is unlikely to go anywhere. It's just too powerful of an optimization tool, especially as memory is one of the things that's just not getting much faster. If you're trying to optimize something like a game engine, being able to traverse your objects as fast as possible means they need to be laid out in memory sequentially. This is being somewhat branded "data oriented design" in the game world ( https://en.wikipedia.org/wiki/Data-oriented_design https://en.wikipedia.org/wiki/Data-oriented_design ), but it's broadly applicable.
Similarly you'll see things like arena allocators in all sorts of places (like protobufs https://developers.google.com/protocol-buffers/docs/reference/arenas https://developers.google.com/protocol-buffers/docs/referenc... ). This kinda blurs the line between "manual memory management" and "garbage collection" since it's sort of both, depending on which side of the usage you're on (individual allocations end up essentially GC'd, but the entire heap is manually managed). Again, it's just too fast of a tool to lose.
The ability to jump between worlds is a very strong strength of C++, and it's something Rust can mostly pull off as well with the unsafe escape hatch.
- kaba0 4y ago> The ability to jump between worlds I feel like it is not possible from low level language, but contrary, it is possible from managed languages. It is easier to give a small escape hatch in a GCd language to allocate n bytes and manipulate it as if you were in C, then to add some hacky semi-GC to C/C++/Rust. Don’t get me wrong, unqiue pointers are really cool and useful, but shared pointers for example can’t be that painfully implemented without full control over execution (thinking of circular dependencies here).
- kllrnohj 4y agoManaged languages almost never offer comparable escape hatches. Eg, it's not possible to do anything interesting at all with the JVM. Nor with JS. Nor with Python. Etc.. None of them let you do either placement allocations nor arena allocations. .NET at least lets you do a linear array of structs, but that's nearly it.
- kaba0 4y agoJVM: bytebuffer, recently https://openjdk.java.net/jeps/393 https://openjdk.java.net/jeps/393 for even more low-level access. Python also has similar techniques available. .NET indeed leads the bunch though. But arbitrary pointer arithmetics is not necessary for arena allocation and safe usage.
- kllrnohj 4y agoNone of those let you allocate objects from those arenas. You can't deserialized into a ByteBuffer. Hell, you can't even do linear object arrays in the JVM (still waiting on value types...), the most basic of memory optimization techniques. Also for a JVM or Python, you can't avoid the extra overhead in an object (such as the ref count for python), which increases cache pressure and limits how much cache locality you can exploit.
- kaba0 4y agoBecause objects in these languages don’t have a uniform representation, it is up to the implementation how a java object is layed out in memory for example. But an allocated memory region is just a.. memory region in java and in C++ as well - you can store your custom data structure in both. Sure, value types will be a huge win, but it is not like you can’t store your RGB class’s data quite trivially into a huge linear structure as SOA or AOS as needed, and deserialize as needed.
- PaulDavisThe1st 4y ago> std::make_shared. At which point it's mostly automatic memory management. Ignoring the awkward issue of circular references, yes. But that's not always trivial to ignore.
- kllrnohj 4y agoimo one should always strongly bias/default to unique_ptr, at which point you just can't have circular references. shared_ptr should be used sparingly, not just because of performance but also as a matter of architecture. You should generally have an organized understanding of ownership, which unique_ptr helps with (and would be the default mode of Rust)
- pimeys 4y agoI'm a bit curious of using `shared_ptr` for all allocated objects, wouldn't it be a bit like using `Arc` with every allocated object in Rust? This means in a tight loop the atomic counter becomes the bottleneck. How is C++ getting around this problem? In Rust I try to avoid `Arc` if I really don't need it due to its behavior that is hard to track in certain situations. I use it to share items over thread boundaries by value, that's it.
- clappski 4y agoIn practise shared_ptr should only be used when you need to actually share ownership of some dynamically allocated object. You get around it by using it in the right place. You’re correct on the refcounting being a performance issue. Codebases that use shared_ptr for everything because they think it ‘solves’ having to manage memory are smelly, it shows that no one has thought about the ownership model. Not to say there isn’t a solid use case for it, but some developers use it by default because they don’t fully understand the trade offs/why it’s important to think about ownership of managed memory. As an example in our medium to large code base we have two places we use a shared_ptr, completely off the hot path and for long lived objects that aren’t copied more than a few times, but need to share ownership of the pointee - in a similar use case of your use of Arc. Almost all of the time unique_ptr, with a single owner, is exactly what you want to replace the memory management side of a dynamically allocated object. From there you will pass around raw pointers or references to the object managed by the unique_ptr (for functions that need to ‘borrow’ the object). Something interesting about shared_ptr is how it relates to weak_ptr, although I’ve never seen weak_ptr in anything out side documentation!
- pimeys 4y agoIsn't `weak_ptr` the same as `sync::Weak` in Rust? There are a few places I've seen this in use. One of them is if you want to build a tree of data you can traverse in any direction. E.g. parent -> child and from child -> parent again. One of the directions must hold a `sync::Weak`, otherwise we leak memory. We've been going around this issue by storing flat collections instead of structuring the data as a tree. We for example have parents and children in adjacent vectors, and parent only needs to know the ids in the children vector to be able to iterate over them. In this case you can do a data structure storing the id values that are `Copy`, meaning this whole data structure can be `Copy` and is super easy to reason about. The other use-case is a database connection pool. When you take a connection out from the pool, you get a reference to the pool itself. The pool is quite often internally inside an `Arc`, and the connection gets wrapped into a guard object holding a `Weak` to the pool. With RAII when the connection drops out from the scope, the `Weak` is checked is it still valid (is the pool alive) and if so, the connection is returned back to the pool.