The Simplest Change to Improve the Performance of Your C++ Code
How a single character can make your games crash or thrive.
Hello there!
Welcome to Algorithmically Speaking, where we discuss topics on the intersection of Computer Science, Software Engineering, and life.
Today’s topic is a guest post by
, a former competitive programmer renowned for his extensive experience in C++, computer graphics, and game development.This is the first post in a series that delves into the intricacies of C++. This series is a unique opportunity to learn about this beautiful programming language from a savvy computer scientist with hands-on experience.
Buckle up!
Great things are not done by impulse but by a series of small things brought together.
— Vincent van Gogh
Let's kick things off with a look at some code.
Imagine you’re working on a game engine and have a function that checks collisions between objects. Naturally, you pass in a list of game objects—let’s say, 1000 of them—to this function like this:
Seems fine, right?
After all, std::vector<GameObject>
is the exact type we need, and the function just does its job. But here’s the twist: this code is silently hogging memory and processing power, all because of how we’re passing in that list.
Since we’re passing objects
by value, C++ creates a full copy of all 1000 elements. That’s not just 1,000 game objects in memory—it’s 2000 once the copy is made! In game programming, this duplication can mean you’re doubling hefty assets like textures, models, and animations.
Imagine each game object with its own textures, animations, and models—duplicating all that can quickly drain memory and drag down performance. Suddenly, your game might slow down, drop frames, or even lag—all because of a hidden cost in how the objects are passed.
The good news?
We can fix this by adding one character. But conceptually, this small change is actually a big one, so let’s explore why C++ works this way and how to avoid these hidden costs.
But first, let me quickly introduce you to C++!
What Is C++?
C++ is often misunderstood, especially by those just getting started.
Some people think it's a program you can download to start coding immediately. But C++ is a language specification—it’s like a set of rules describing how the language should work.
To write and run C++ programs, we use a compiler, software that turns your C++ code into machine code that your computer can understand.
Creating a C++ compiler isn’t exactly simple, which is why only a few have become industry standards. You might have heard of GCC (GNU Compiler Collection), MSVC (Microsoft Visual C++), and Clang—these are some of the most widely used. While they all adhere to the same C++ rules, they have their own quirks.
For example, they might display different error messages, compile code at different speeds, or optimize your program in unique ways.
To see what I mean, let’s take a look at this code snippet with a deliberate typo:
I intentionally made a mistake: the variable should be myVariable
, but I wrote mVariable
instead. If we try to compile this code, here’s what the error messages look like with different compilers:
(GCC) error: ‘myVariable’ was not declared in this scope; did you mean ‘mVariable’?
(Clang) error: use of undeclared identifier 'myVariable'; did you mean 'mVariable'?
See the difference? Both compilers are flagging the same issue, but the wording varies.
So why are there multiple compilers if they all do basically the same thing?1
Well, each one brings something a little different to the table. Some prioritize speed, others offer better debugging features, and some are just better suited to certain platforms.
This variety gives developers the flexibility to pick the best tool for the job, whether maximizing speed or monitoring memory usage.
Getting to Grips with C++ Parameters: Value vs. Reference
Before jumping into a solution to the initial problem, let’s get comfortable with how C++ handles function parameters. Understanding this is crucial, and it can often be the key to writing efficient and high-performing code.
So, let's explore why pass-by-reference can often be the better choice—but remember, it's not always the answer!
The Basics of Parameter Passing in C++
When we talk about “passing parameters” to a function, we’re describing how data moves from the caller to the function itself. C++ offers a few methods to do this, each with unique trade-offs:
Pass-by-Value
This is the default in C++.
When you pass a variable by value, C++ creates a copy of that variable specifically for the function to use. While this approach is safe and prevents the original variable from being modified, it can be inefficient for large data types.
Imagine passing a structure or class containing numerous fields—each function call would require C++ to create a duplicate, taking up extra memory and time.
Pass-by-Reference
When you pass a parameter by reference, the function works directly with the original variable rather than a copy. This allows C++ to save memory and time since no duplication occurs.
References are indicated in the function declaration by the &
symbol.2
What’s a Reference, Anyway?
In C++, a reference essentially refers to the memory address of a variable.
While the term “reference” might sound confusing, it just boils down to a number representing where the data is located in memory. When you pass a parameter by reference, you’re just passing a pointer (an address) to the variable.
So why not just use references everywhere?
Passing by reference isn’t always worth it, especially with smaller data types like int
or float
, which don’t benefit as much from reference-passing due to their minimal memory footprint.3
A Practical Look: Pass-by-Reference in Game Development
To illustrate, let’s examine a common function in game development: lerp
, or linear interpolation.
Lerp helps create smooth transitions in animations, like the position of a character moving between two points. We will explore its applications in a future post.
Here’s an example of a lerp
function where all parameters are passed by reference:
This approach technically avoids copying the parameters, but there’s a hidden cost here.
References in C++ are implemented as pointers under the hood, which means each reference stores a memory address. On a 64-bit system, a pointer takes up 8 bytes, so the three references here occupy 24 bytes in total!
By contrast, if we pass these variables by value instead each float
takes up only 4 bytes.
In this case, passing by value saves memory since we’re only using 12 bytes instead of 24. For small data types like float
, int
, or char
, copying values can sometimes be cheaper and faster than managing memory addresses.
You might think that 24 bytes isn’t a big deal, but every single byte counts in game development!
It’s like trying to squeeze the last drop of juice from an orange—every bit matters when you're optimizing performance. Those seemingly small amounts can add up quickly, impacting load times and overall gameplay experience.
So, let's not underestimate the power of those bytes.
How Much Memory Does a Pointer Use?
Earlier, I mentioned that a pointer takes up 8 bytes on 64-bit machines, while 32-bit machines use 4 bytes.
It's essential to be aware of this difference, especially when considering the hardware your program might run on.
To determine the exact size of a pointer in your environment, you can use C++’s sizeof
operator:
A Simple Change, Big Impact
Now that we better understand the mechanisms of passing parameters to functions in C++, the fix to our initial problem is simple: use a reference.
By passing our vector
of game objects as a reference, we avoid duplicating data in memory. Here’s the optimized version:
With const std::vector<GameObject>&
, we’re now working directly with the original vector
—no extra copies are made, and const
ensures we don’t accidentally modify it.
With this single tweak, we’ve transformed our function to be both faster and more memory-efficient.
So, next time you write a function, think about how you pass your data.
Passing by reference can help keep your game running smoothly for extensive collections or complex objects.
Just a simple &
can save the day—and your game’s performance!
Conclusions
I hope these insights have sparked your curiosity and left you eager to explore the intricacies of C++ further.
There’s so much more to explore, and in the upcoming episodes, I’ll cover topics like static variables, smart pointers, the pitfalls of using the std namespace, and the right way to work with vectors.
We'll also delve into the power of inline functions and variables, practical design patterns for graphics programming and game engines, tools to streamline C++ compilation, and plenty of other tips and tricks.
Stay tuned—there’s a lot more to uncover!
Until next time,
Jesus
Did you like this article? Make sure to 💛 click the like button.
Is there something you want to add? Make sure to 💬 comment.
Do you know someone who would find this helpful? Make sure to 🔁 share this post.
The latest from Algorithmically Speaking
Practical Software Development Skills from Competitive Programming
The importance of data when making decisions in the engineering industry
Here is how I can help you further
Are you interested in my book on graph theory? Check it out.
Are you interested in sponsoring this newsletter? Please reach out.
Get in touch
You can find me on LinkedIn and GitHub.
This newsletter is funded by paid subscriptions from readers like yourself.
If you aren’t already, consider becoming a paid subscriber to receive the whole experience!
Please let me know if you want me to cover any specific topics of interest to you. I’m more than happy to do so.
Have a great day 🌞,
Alberto
You may also wonder why anyone would bother creating a C++ compiler in the first place. Besides the serious reasons, there's always the option of doing it for fun. After all, some people just enjoy a good challenge!
There is also the Pass-By-Pointer mechanism, with the difference that instead of passing a reference, we pass a raw pointer itself.
When you think about it, C++ only allows passing by value. References, essentially pointers, are just values—they represent memory addresses (basically numbers).