Hello, and welcome to C++ Weekly. I'm your host, Jason Turner. I'm available for code reviews and
on-site training. In this episode we're going to spend some time looking at Clang's time-trace
facility. We will apply it to a real world project and we will attempt to actually reduce the compile
times of this real world project based on the knowledge that we gain from this analysis.
Now before we get to that I do want to remind you that I am available for training. There
is a link in the video description for how to get here to see more about my training offers
which are geared towards companies. I have been working on books related to C++, which you can
find links to in the video description as well, and all of these episode ideas are driven by you
from my cpp_weekly project on Github, so make sure you make your interest known for what episodes you
would like to see. Okay, so let's start with this analysis of our build time. I am actually going
to look at Chai Script, which is my scripting engine that I started on quite some time ago with
JT, whom you may know from the live streams that we have done together. So I'm in the CMake
configuration here and I'm using ccmake which I actually do prefer for visualizing all of the
options that have been chosen here. Now I don't have any fancy things enabled that might throw
off this build time calculation, so I am not doing any sanitizers. I'm not doing any extra analysis,
I just have gone in and I've hit "t" in ccmake here to toggle advanced mode; and I've added
this extra flag for cmake_cxx_flags_debug of -ftime-trace, and again this is with Clang.
So once the program has been configured with that option we just want to compile,
and I'm going to do a quick 'make clean' to make sure that nothing is going to throw us off here,
and just for good measure let's go ahead and clear our ccache as well. Okay, so I'm going to do a
"usr/bin/time make chai," so I'm just going to build the main executable and I'm not going to
do a parallel build for this particular time test. We'll see the results of this in just a moment,
although I might enable verbose=1 here just so we can confirm that we are in
fact passing the flags that we want to to our build, and I'm doing a debug build.
So if we want to we can actually look in here and see what warnings are set and what C++ standard
is set, and we can see this -ftime-trace. We're doing a debug build and you can see the debug
flag has passed as well. Now interestingly, clearly, Chai Script needs some update. I
haven't worked on it personally in quite some time, but we can see that we are getting warnings
with our modern Clang analysis here telling us that we're using reserved identifiers
in this short_alloc file that we borrowed from someone else. (I) Should probably take a look
into that. I will speed these builds back up later so you don't have to sit here through the
whole thing. Okay. So what we saw is one minute and four seconds to compile these three files,
and we're going to go down into this directory where main.cpp.o was created so you can see
here we are in this "ChaiScript-build time/build/ CMakeFiles/chai.dir/src." What you might observe
is this json file that has been created by the compiler for us. This is intended specifically
for inclusion in Chromium actually, or Chrome, Chromium, depends on what version you're using
but the Chrome engine; so if you've never seen this before you just open Chrome and you go to
chrome://tracing and this is meant for the visualization of,
well, trace logs. So we're going to go ahead and load up this map.cpp.json file
and we see a flame graph of everything that the compiler had to do to build our code. Now
there are approximately three things that we want to look at using Compiler Explorer as a
notepad here, as one does of course. We need to, again, look at three things.
We might have expensive header includes that should be moved to our list of pre-compiled
headers. That's not a great solution for Chai Script because that doesn't affect the
average user of Chai Script unless we want to require them somehow to use our same PCH list,
so I'm not going to spend a lot of time looking at that. Number two is template. This is looking at
template instantiations to reduce or eliminate - we're looking for particularly expensive
templates and thinking about whether or not we can de-templatize code. Now
creating new template instantiations is rather expensive in the compiler,
and number three is function templates to extern for our users. Now I strongly recommend
that you check out my episode on 'extern template' for functions. That might actually be a very good
use case for Chai Script but it has a minor problem and that currently I guarantee that
Chai Script is a header only scripting library, so if you actually wanted to use it after I
provide these 'extern template' things then that adds an extra level of complication for the users.
Now that is specifically episode 330 that you want to check out for more information about that.
Okay, so here we are back at our flame graph, splitting this up-front end and back end and
these source things. We can just click on this, and we can see that iostream specifically
spent 357 milliseconds wall clock time being parsed by the compiler, so that is something
that we could move to our PCH list. All right, now the next large source file that we see is,
oh goodness, that's std::regex. Now we all know that std::regex is expensive to include,
so that's an extra 322 milliseconds on the wall clock for including regex. What a shame.
Now the next thing is chaiscript_basic.hpp. That's 1.2 seconds just including and parsing
that file. Now that really sounds like something that should be added to the PCH list if you are
using Chai Script as a user. I mean, just go ahead and do that right now. If you are a user of Chai
Script - add this header file to your PCH list and you just saved 1.2 seconds on every single
cpp file that includes it. These are largely broken down into other Chai Script header
files underneath here, and if you want to you can start digging in and seeing that parsing this one
particular class, parsing the class Boxed_Number, took 148 milliseconds all by itself; and parsing
the class Dispatch_Engine took 222 milliseconds. (Turns out I can zoom this interface for you a
little bit here.) Moving forward, we've got this PerformPendingInstantiations and then we have this
OptModule down here. This is six seconds, um, on the back end, so this is dealing with all of the
types that have actually been created. We're seeing all of the mangled names in here, so
I don't think we can directly affect this unless you can reduce the number of types
that are being generated at compile time. We can use this one here to zoom in - this
is make_unique CodeGen Function. Interesting. make_shared, unordered_set, _Hashtable. Okay,
and we see if we have make_unique calls __unique_ptr __detail (or creates that type),
which creates a __unique_ptr_t, which creates this _MakeUniq thing, which creates a unique_ptr,
which creates some other internal make __uniq_ptr_data thing and __unique_ptr_impl
thing and a tuple. Tuples are used internal to, here we go, internal to unique_ptr. That's
interesting. Let's see, what is this? Okay. That's a different make_unique call.
Vector of module_errors, okay. So we can start to get a feel where if you instantiate
too many different vectors, too many different unique_ptr's, that kind of thing
then it starts to cost... Oh look, another make_unique pointer. Oh look, another make_unique.
Okay, another make_unique. All right, we're starting to be able to recognize
this pattern right here. This is clearly a make_unique stalagtite, if you will,
and they're... they just keep going - there's so many of them. So let's go
ahead and look in these Pending Instantiations as well. Let's see - Instantiate Function,
so this is all the function wrappers inside of Chai Script, and we have this make_shared helper
here, so there's possibly something to look at here in trying to reduce the amount of time it
takes to compile the Proxy_Function_Callable_Impl. That looks like it would probably be a good place
to look, and we already identified that our uses of unique_ptr seem to be coming up. Let's see,
yeah, Boxed_Value of float, Boxed_Value of long, Boxed_Value of char, so, why though?
Let's see that creates a Any to a shared_ptr which has make_unique in it. Interesting. So
we can start to see again all of these calls to unique_ptr, and there's many different
instantiations of unique_ptr. That's definitely something we should dig into. So all of that code
was down in the Any header, and this is kind of my own implementation of std::any, and this
has a long history for why I did it this way, but the specific call to make_unique is here.
So I'm calling make_unique. I would say that I need to make a Data_Impl. Now if you watched
my more recent episode on a simplified version of std::function it's the exact same technique here.
My Any object has a unique pointer to some sort of type erased data type so that I can pass this
thing around, copy it however I need to, and then internally there's actually a type erased thing
and it has to do a runtime dispatch. You can see this override here on this clone function.
Okay, so that's so that I can make a copy of it. All right. So this is interesting because
I'm following best practices here - I'm saying, "I need a unique pointer of data impl of T,
and I'm passing it this pointer m_data that I want it to make a copy of this object here," basically
explicitly calling the copy constructor. Now at this point I want to be very careful with what you
take away from this episode. I am not telling you to do what I'm doing, I'm telling you to be aware
of all of the different types of templates that you instantiate when you are trying to optimize
your builds. So if I were to actually commit this change I am going to need to make
comments as to why I did that, but what I really need to do is reduce the number of unique_ptr
template instantiations that exist in Chai Script, and I'm going to do that
in a very unfortunate way. I know that I ultimately want a unique pointer of Data,
and I am going to create one of those by not using make_unique - I'm going
to create a unique pointer of data and I'm going to pass it a Data_Impl.
Now this works because Data_Impl is derived from Data. Because this code is called in so
many places, it is going to instantiate even one more function call then we ultimately need. Now,
again, this is hyper optimization of our compile time, so to avoid any hint of this unique_ptr
of Data_Impl of whatever this templated type is, I'm going to call 'new' on the Data_Impl,
static_cast that to a data pointer, and pass that explicitly to unique_ptr. Now note this
is a safe thing to do because I have properly provided a virtual destructor in the base class.
Okay. I've eliminated both calls to make_unique in my any.hpp and we're gonna see if I got my syntax
right - there's a lot of angle brackets and parentheses to keep track of here.
Okay. There we go. This is at 49 seconds wall clock elapsed. This is 47.5 seconds user time;
and uh, that was 59 seconds user time, or 104 seconds wall clock elapsed previously. That's
pretty good. We went from basically
a minute (59.8 seconds, 60 seconds) to approximately 47 and a half seconds. That's,
uh, that's good right? That's, like, about a 25% compile time savings in Chai Script. Um,
I mean, that's that's kind of ridiculous. That's without even really trying that hard, we just
looked for the common thing that was instantiated over and over again and we reduced it.
So just to be clear, if I was going to keep this change I really do need to put a comment here.
It would be something like this, and then to avoid the static analysis warnings I would have to,
with the consent of the rest of my team, put a NOLINT and this comment here. This tool
-ftime-trace can point you in the right direction and you can learn a lot about your code.
So we saw how you might identify files for adding to PCH, we saw how you might identify expensive
to compile functions that you could move to 'extern template'. Now that wouldn't
have worked for make_unique in this case because there's just far too many different make_unique
function templates being instantiated, but we did see a way that we could kind of
de-templatize this a little bit in a way. We we kind of just avoided instantiating
a whole lot of extra templates and it saved a lot of compile time for a very simple change,
so I do recommend that you also check out this article from the author of libfmt,
again, about another technique for avoiding template instantiations basically. So I will have
a link to that also in the video description, or at the very least in the Github issue for this
video for you to check out. So thanks for watching this episode of C++ Weekly. I hope you learned
something. I hope you treat your new found powers very carefully, but hopefully we can, you know,
slow down the heat death of the universe by avoiding so much CPU time on our builds.