« Why macros rule... | Main | You need a frame allocator! »

09/13/2012

Comments

Feed You can follow this conversation by subscribing to the comment feed for this post.

MikeNicolella

Indeed, great post. I do see people often getting shiny new tools and not using them *just* for the problem they solve. Like smart pointers - "oh, smart pointers? Okay, that means T* with shared_ptr everywhere".

You make good points, but I'll point out a few things about how smart programmers use smart pointers :)

- don't create a base class and insist on inheriting from it to participate in refcounting. The argument for this is usually "if you don't do this, you have 2 dynamic allocations - one for the refcount, and one for the object. And then they also might live on separate cache lines". The inheritance route is called "intrusive" refcounts, and it's not necessary. Your data shouldn't care if it's being refcounted, and it certainly shouldn't need to inherit from some base class bloating its memory size and throwing off its alignment. Also, this means it's hard to refcount a single int, which is important to strive for when writing generic code ("if you want it to work with any T, make sure T can be int"). If you have data that you want to refcount, when you allocate it, allocate the refcount in the same allocation. Look up std::make_shared - this is what that does.

- secondly, *if* you actually have a use case for shared object ownership, this is completely separate from passing objects of T to functions. This is addresses some of the runtime overhead you point out. You shouldn't be passing smart pointers by value, *nor* by reference. Why are you passing smart pointers?! If you have void foo(shared_ptr const& t); you almost certainly meant to write void foo(T* t);. Almost every function that takes an object of type t doesn't care about how its lifetime is managed. By doing this you're also saying foo() can only be called on T's that are wrapped in shared_ptr - what if you have unique_ptr, or a 3rd party refcounting smart pointer? The only exception to this is if the function *must* participate in sharing ownership, and in that case it's obvious that it would be strongly coupled to the type of lifetime management being used. Pass raw pointers. The cases where it is correct to pass a smart pointer is obvious because it won't work any other way.

So, the only place I really see smart pointers existing is as members on objects. And if you have a piece of code (or deep chain of functions) that needs to operate on that object, you pay the dereference cost *once* at the very top, and pass a T* or T& down the chain - this eliminates the 'death by a million papercuts' you get when you inc+dec the refcount uselessly everywhere. Agreed that this is an extremely difficult cost to measure. The top function that does the dereference likely already at least implicitly keeps the object alive, just by the fact that you're *in* that function, so there's no reason to increment it.

Now, I don't have a good list of valid uses of smart pointers. I used to be careless and just use them wherever. Nowadays they don't show up very often. *Especially* now with C++11 move semantics, unique_ptr is the default go-to. It's a non-copyable single-owner pointer, which is what you almost always want. The issue previously was that sometimes this "needed" to be a smart_ptr because C++ implemented moving with copying, so this didn't work semantically (auto_ptr). Now that moving is a C++ concept, unique_ptr wins and is as efficient as a raw pointer - it's really just used to trigger compile errors where you accidentally copied the pointer, which is what you want.

It's just not that big of a problem in games. You already should have object ownership laws, and they must be obeyed. I see shared_ptr often being used as a bandaid - you use shared_ptr so that you don't have a dangling pointer. But your real problem is that your ownership is fuzzy or not being respected - you're trading a dangling pointer for a memory leak. Use unique_ptr and these design bugs will now throw compile errors.

Lastly, about weak_ptrs. Do *not* give your weak_ptr a "convenient" operator-> that does the lookup, ref increment, and return the pointer. Do you really want to do a table or hash or whatever lookup on every -> ? (hint: no) Your programmers won't notice what's going on - they're copy+pasting code that does this and slowing your shit down. I worked in a codebase that did this - it was horrifying how many times the pointers got looked up for validity every frame. Also, the code was almost never correct written this way, because technically the pointer could go null halfway through your function since you never actually took ownership. Always turn weak_ptr into shared_ptr before getting at the object.

Phew :)

Stephen Nichols

Yeah, you sure can mitigate some of the performance issues by implicitly casting the smart pointer to a raw pointer. Good point. I observe that very few code bases that use smart pointers do it that way. It's as you used to do... use smart pointers everywhere!

Oh, I really love when I see std::vectors of smart pointers. Makes me cry inside. :)

Stephen Nichols

Oh, and I threw up a little when I read your "let's be able to ref count an int." Sure, I get that generic is nice with any type. And an adaptor that adds ref count functionality to any type is nice. But, it's a bit overkill to add ref counting to ints. Silly dude.

MikeNicolella

Well, testing if it works for 'int' is more of a thought exercise to see if your code can be used with objects where you can't just change their code - think of classes in 3rd party libs. It turns out 'int' is a nice portable 3rd party lib that fits the bill :)

BZ

Haha.. you use a memory manager? Do you need mommy to wipe you after potty too? Real memory managers are a notebook filled with relative addresses and a comment on the way the application is using them.

Stephen Nichols

You know how retarded that sounds, right BZ?

1. You say "you use a memory manager?"

2. You then proceed to describe what a correct memory manager looks like.

If your argument is that static allocation of all data is the way to go, I can agree with that on some level. But, clearly, static allocation is NOT the correct choice in all cases. For example, would you like your web browser to preallocate all the RAM it needs to service the largest web pages possible? Clearly that's a bad choice.

Sounds like you just write simple programs that rely on fixed memory footprints. One day you'll graduate to writing real code! Keep at it...

Victor

With this post I'm a fan.

GP

Dunno but, I'm one of those guys who likes to keep control of everything, i don't like to guess i a;ways want to know what's happening behind curtains. I agree with the article. Kudos!

Z Yezek

Smart pointers and other C++ tools are like any tool- they don't fix stupid. But that's the case with literally everything in programming.

Frankly, you're blaming a concept for a particular crappy code base, one that by your own admission DIDN'T FULLY OR PROPERLY USE IT. That's like saying GPS is a terrible technology because it sometimes causes dumb & careless people to drive off cliffs.

As to your specific technical criticisms, many don't hold water. Any scheme for ensuring objects are usable across threads and have deterministic lifetimes is going to have non-zero overhead. If you are doing highly async processing, making objects responsible for their own thread safety and lifetime is nothing more than a modern update to the concept of encapsulation. And guess what buddy, reference cycles are a LOGIC FLAW that no amount of syntax or objects will cure you from, for the same reason no nifty new computer language is going to solve the Halting Problem. If you have them you'll end up just as dead with raw pointers as anything else.

The comments to this entry are closed.