Hello and welcome to C++ Weekly. I'm your host, Jason Turner.
I'm available for on-site training, contracting, and code reviews.
In this episode, I'm going to show you one simple trick to reducing code bloat
in your programs and making performance better.
It might seem a little bit clickbaity,
and I apologize a little bit for the title.
But, seriously, this is something that I have
seen many times. I have done it myself, and I don't believe I've
ever mentioned on the channel before, but I always tell my students this when I'm teaching classes.
Every single mistake that I teach, and I say,
"Don't do this thing," it's something that I have done.
Let's look at this and see if you are doing this
in your code. I'm guessing you probably are somewhere.
So let's start with a very simple struct that has the very creative name of 'S',
and it's storing a single integer.
We're going to put this into vector. So I'm creating this
vector. No big deal. A single default constructed 'S'. So
that's all we've done. We've created a vector. We've put one object in it.
Now this should be relatively simple code, and we can see that we have called
an 'operator new' here. This is where it is allocating the memory storage for the vector,
and somewhere in here, it's setting up that new object, and then it's going to do whatever it
needs to do, and then it's going to delete it. We're probably seeing extra calls in here
for handling the case of whether or not it needed to resize the vector.
So right now the vector is the size of zero.
We emplace one back, and that's going to be a size of one; but it has to deal with the question of,
"Was there previously allocated a buffer that I have to resize?" which you would think it might be
able to see through, but it seems that GCC trunk here is not.
So let's go ahead and add a clone here, and we're going to
make this Clang trunk. Clang is going to be interesting here. So it does a 'new',
it sets up the data, adds the zero in here, deletes it. OK, fine.
I have worked with plenty of code (and this is the point
of this episode, I don't want to get bogged down too much in what exactly 'vector' is
doing) where we define a destructor kind of just out of habit.
We're like-- "Here's a destructor." So somewhere else (now I can't actually
put the code here because if I do, the compiler can see through this and it will mess up some of
the point that we are trying to make) you have an empty destructor or a destructor that you're like,
"Well, I need to set 'i' to zero before this thing goes out of scope."
Now the compiler here has to actually do this. It has to make a call to destructor somehow.
It has to make a call to this destructor. It has to deal with this lifetime here.
And you can see here, this call to the destructor. So this is 13 assembly instructions, whatever,
calls to functions. It's not that big of a deal. The one at the GCC on
the other side is 165. (GCC is not quite as good
with some of the optimizations that Clang is in these particular cases.)
Now, if we take this out so that it actually is visible to the compiler,
both of them can see this. They'll inline it, and it will simplify the
code considerably because it knows it doesn't have to do anything with this object lifetime.
Now, if we do a second one like this, then we're going to see
quite a bit more code generated because it has to do a 'new', and then it has to do a second
'new' when it resizes the vector, and a delete, and a delete.
Now let's look at the Clang side here. We've got 68 lines. Now, I'm simplifying things a little
bit, but I want to make a point. We have multiple things going on.
This reduced from what? 68 to 58 because it doesn't
have to generate these calls to the destructor;
and if we removed the destructor altogether here, it goes down to 28.
Now why does it go down to 28? We've played with some
of this in the past. This is partially because it has not generated our move operations for us
by defining our own destructor - we've implicitly disabled move operations.
And this is just a struct with an 'int' in it. Let's go ahead make this a struct with a string.
And believe me, I have seen this code in the real world before.
We do this, we've got, again, 58.
Go back to this. Let's make this an actual string here. So this is 231 lines of code on
the Clang side. We put in the destructor again. Now, with all of our disabled move operations
on the resizes and such, it's going to have to do copies all over the place.
This is terrible. It can't optimize it nearly as well, and even if we inline this like this,
it's not going to give us as good of results as we could have had if we just simply did not
define the destructor at all. So if you don't have a reason to have a destructor, don't have
a destructor. This can make an absolutely gigantic difference in your code. I promise, somewhere in
your code, you're doing this. You have empty destructors defined out of habit. Follow the
Rule of 0 - get rid of them. This can clean up move operations. It can make all kinds of things
better performance. If the compiler knows that something is trivially destructible, that's huge.
So the specific example that I saw recently was something like this - this is kind of the
pathological case. There's really no reason at all to have this destructor clearing out this member.
And if you really, really wanted to do that, well just don't. I mean,
I could say, "Define it inside the struct." Now, I will admit - and I know that there's
at least one person watching this episode that will comment that sometimes it is necessary to
define your destructor in a PIMPL idiom kind of case because the compiler has to know that
there is an upcoming destructor because it can't inline it when it's inlining the destructor call
for the unique pointer wrapper for your PIMPL that you've set up.
Yes that's true, but make sure you follow the Rule of 5,
and make sure that you're smart about this thing. You know,
tell the compiler that this is, in fact not, you know-- it's a noexcept destructor.
That it's-- define your other special member functions, your move constructor,
your move assignment, and everything.
You still have to follow these rules to make sure
that you give the compiler as much information as you possibly can.
So that's my simple rule. Stop defining destructors when
you don't have a reason to. Follow the Rule of
0. And thanks for watching this episode of C++ Weekly.