C++ Beyond the Syllabus #1: Peeking under the hood with Compiler Explorer
If your code does not behave as you intended it to, something is wrong. This is obvious.
Even if your code does produce the correct output, though, it can still be doing so in unintended ways, which is still wrong. This is less obvious.
It is generally a good practice to take interest in how your code does what it does… especially when you are first exploring new features.
I am far from an expert on compilers (as like most people) and that’s exactly why Compiler Explorer is my go-to first step when comparing different code implementations or learning new features.
Compiler Explorer, also known as Godbolt, is a free online tool first created by Matt Godbolt in 2012. The tool allows you to:
- write code (in most common coding languages)
- select your compiler and compiler options
- immediately see the generated disassembly
- execute your code
- generate a URL to share code + configurations with others
- tons of other features
In this post, I’ll show you how to use all of the above features along with other tips and tricks. By the end, you’ll feel comfortable (and see the value in) exploring your own code in Compiler Explorer.
One of the best ways to learn the thing, is to do the thing! You can follow along at godbolt.org.
Language & Compiler
When you first open up Compiler Explorer, you’ll see something like this.
From the language drop down, you can select one of many languages, including C, C++, CMakeScript, Go, Java, Javascript, Python, and several mobile development languages.
To see what compiler you are running in your development environment, you can open the terminal and enter the command c++ — version
. View the output and you will see the needed tidbits of information, though it might not all appear on one line. For me, you can see my local setup uses the compiler x86–64 clang 14.0.0
.
You can then go ahead and configure the compiler in Compiler Explorer to be the same, or experiment with other configurations.
We can also set compiler options to configure optimization level, standard library version, and more, which I’ll dive into after a quick refresher on everyone’s favorite topic… assembly code!
Deciphering assembly code
If you’re anything like me, you sat in your university’s Intro to Computer Architecture course looking at the assembly code on the lecture slides thinking something along these lines: “This is some voodoo magic that C++ is built on. I’m never actually going to need this. When can I get back to my real coding project?”
Here we are. Present Jared is now looking back on sophomore-in-college Jared thinking, hey, I get that you didn’t see the value back then, but I’m thankful you at least took notes and decided to keep them!
The good news is you don’t really need to know the intricacies of assembly (I sure don’t) unless you plan on writing a compiler anytime soon. Here’s the Sparknotes:
- Registers hold temporary values or addresses, which are being actively operated on
- Registers are 4 or 8 bytes each based on whether your system is 32- or 64-bit
- While some are specialty registers, most are for “general purposes”
- A quick google search will show you the name & purpose of each specialty register for your compiler
- One of the most common specialty registers is
eax
orrax
(system-dependent name), which holds the return value from each stack frame when returning execution to its caller
There are a lot of assembly instructions, usually of the format instruction dest_reg, src_reg1 src_reg2
. Here are some examples…
mov dest_reg, src_reg
— copies contents ofsrc_reg
intodest_reg
add dest_reg, src_reg
— adds the contents ofsrc_reg
anddest_reg
, storing the result indest_reg
and dest_reg, src_reg
— bitwise-ANDs the contents ofsrc_reg
anddest_reg
, storing the result indest_reg
push src_reg
— moves contents ofsrc_reg
to the top of the stackpop dest_reg
— moves contents from the top of the stack todest_reg
Luckily, Compiler Explorer makes it easy to learn new instructions. First, when you hover your cursor over a line in the source code, the associated disassembly will become highlighted. This allows you to easily narrow your scope of how much disassembly you look at at a time.
Then, you can hover over each instruction to see a snippet of the documentation and get a link to more information.
To see even more information, right click on the disassembly instruction and select View assembly documentation
.
With these tools, Compiler Explorer will help you navigate your way through unfamiliar disassembly.
Additional Panels
You can add additional panels to your Explorer for side-by-side implementation comparisons. To add another source code panel, select Add New
> Source Editor
as seen below.
From there, you can drag and drop the panels to any configuration you’d like. To view disassembly for the new source code editor, select Add New
> Compiler
from the menu on the second source editor.
After adding a new compiler window, make sure to update the default compiler and any compiler options. Otherwise, it will be set to the default.
You will then be able to see generated disassembly code for each of the source implementations. In this case, we see how allocating space for an array via the new
keyword requires significantly more disassembly instructions than the default array allocation on the stack.
Window Templates
In addition to creating your own window configurations, Compiler Explorer has a suite of pre-made templates you can load. Simply click Templates
in the main menu bar.
Then, select from the template options, including various Diff configurations and a CMake starter project.
Executing Your Code
As you may have seen in the above section, we can also add an Execution Only
panel to our Explorer. This will allow you to view any compiler errors or program output. You can see this at work in the short program below.
To demonstrate a compiler error, and lead into our next section on setting the C++ STL version, I will attempt to make the square
function above consteval
and see what happens.
As you can see above, it appears the consteval
keyword is not known by the compiler. Granted this is an old version of Clang, and consteval
was added to the STL in C++20, I suspect Godbolt is defaulting to a previous C++ version.
Setting Your C++ STL Version
How can we confirm the C++ version was the culprit of our issue above? Well, some Googling will tell you, but that’s no fun. Let’s instead write a short program with the __cplusplus
macro described here.
Here, we see that we are using C++14, which explains the error message from our above example. We can override this version by using the std
compiler option, as shown below, and confirm C++20 is now being used.
Let’s go ahead and add this compiler option to our example with the consteval
error from earlier.
Aha! We did it — nice :)
What about compiler optimizations?
Good point. I knew someone would bring that up ;) By default, most compiler optimizations will be disabled in Compiler Explorer, but they are easy to turn on.
Before we dive into that though, let’s take a step back.
Unless you are actively designing compilers, hardware or working with assembly code in-depth every day, making assumptions about code runtime based on the number of lines of assembly code is a bad idea. In fact, maximizing runtime optimizations often involves increasing assembly code size.
In practice, a common performance testing technique called benchmarking is always good to do when considering if one implementation is actually faster than another. (We’ll talk more about benchmarking in next week’s post!)
Each compiler will have their own manual describing the nuances of each compiler flag, though the main gist will be pretty similar across the board. I have linked the clang and gcc documentation here.
Some commonly used flags follow.
-O0
— default setting, which optimizes for compilation time and ensures debugging produces expected results-O3
— enables all C++ standard-compliant optimizations, which often sacrifice compile time and code size for runtime-Ofast
— similar to-O3
with the addition of ultra aggressive optimizations, which may violate C++ standards
Let’s compare our consteval
example with -O0
vs -O3
.
As you can see, the -O3
optimization optimizes away nearly all of our logic (we could have expected this 😅).
In this particular case, the -O3
optimizations shortened our disassembly code, but keep in mind that should not be assumed to be a universal rule.
I’d encourage you to test out different compiler optimizations for yourself, though I usually find -O0
to be the most helpful when learning how a new feature works under the hood.
Sharing Your Configurations
Everything is more fun with friends! One of the best parts about Compiler Explorer is that it is so easy to share configurations with others. On the top right, just click Share
to get a link you can send to friends and coworkers.
I’ve found sharing links to concise examples is a great way to provide context when evaluating changes on code reviews. As a bonus, this often sparks lively discussions about new features and best practices.
Sources & More Info
- godbolt.org
- CppCon 2017: Matt Godbolt “What Has My Compiler Done for Me Lately? Unbolting the Compiler’s Lid”
- What’s New in Compiler Explorer? — Matt Godbolt — C++ on Sea 2023
- Compiler Explorer GitHub
What’s next?
I hope you learned something new from this post. If so, please like, comment, and/or share it with a friend. As a reminder, I am not an expert, but I hope you will still find value in learning alongside me 🤓
This is my first-ever technical blog post, so any feedback would be greatly appreciated.
I have a bunch of other blogs in the pipeline, which I am excited to share with you all. Currently, these fall into a few main categories:
- Real-World C++ Features Not Typically Taught in School
- Concurrency
- Templating
- Non-STL Tools, Libraries & Data Structures
- Low Latency Tips & Tricks
If you have any specific suggestions or requests, please let me know in the comments! See you all next week 😎
Collaborators
A special thanks to a few friends who took time to edit and review this post: