I want to go fast
But what does that mean? For me that has meant learning C++ and, well, building things that go really fast. This means learning a new domain and taking software craftsmanship to a new level. Keep reading and I’ll explain.
Wanting To Go Fast (in C++)
The ability to go fast has always been there in the form of C or C++. But I had always heard ugly rumors, seen ugly code, and had the general notion that it was really easy to shoot yourself in the foot.
This has always made me keep my distance, that is, until my day-job had me programming in a large C-application. And I more or less had my suspicions confirmed that C was not an ergonomic language. It was difficult to use, difficult to read, and not much fun to program in. That’s when I discovered Rust.
Rust looked a lot like Scala — one of my favorite languages — but with a focus on performance, a minimal runtime, and compiling directly to machine code. In this way it targeted much of the domain of C++. However, it also placed a high importance on code-safety and avoiding the common pitfalls of C or C++. There is even a fully functional operating system built on top of Rust, which is a good demonstration of these principals.
I spent a good amount of my free time learning Rust, getting excited about Rust, and leading a weekly meetup to solve problems and discuss solutions in Rust. I was definitely on the Rust train.
And then life happened again.
My day-job once again thrust me back into the territory of C, this time with the addition of C++. It was a mostly greenfield project, which meant that most of my work would be writing new C++ code on top of existing shared libraries in C.
It was, once again, time to dive back into the land of non-ergonomic and difficult code… or so I thought.
If you haven’t learned C++, or only had to touch it during your college algorithms class, then you should know C++ exists in two forms. There is pre-C++11 which can be described as “C with classes” and there is C++11 and beyond (11, 14, 17, and soon 20).
I was more than pleasantly surprised to see many of the “modern” features I enjoyed in Rust also existed in C++. It has “smart pointers” that confer certain ownership properties such as uniqueness or shared (reference-counted) objects. Along with this is RAII semantics which make it easy to avoid much of the code that was so easy to shoot yourself in the foot with.
The latest version (17) brought incremental updates over 11 and 14 to improve the library and add several small features (such as structured bindings). However, the FileSystem TS (technical-specification) landed and is a major improvement in language ergonomics.
C++20 might see some major ergonomic improvements with concepts (similar to Go-style interfaces), networking, modules, coroutines, concurrency features (futures, latches, barriers, atomic smart-pointers), compile-time reflection, and a few other features I won’t mention because they start to get a little deep into the woods.
What I’m trying to say is modern C++ is, in fact, just that — modern. It contains all (or at least most) of the features I’m used to using in a language like Scala. And is a much safer language than it used to be. And best of all, it’s evolution is not stopping. New and exciting features are constantly being added to the language.
It’s fair to say that C++ has been behind the times in terms of language features, standard library features, and tooling (build systems and package managers to be specific). But this is all improving (and at a rapid pace). I’ve even heard this referred to as the C++ Renaissance by long-term C++ developers.
P.S. — If this excites you, check out a new project I’m working on over at cpp-vs.com for a different way to learn C++.
Craftsmanship (Performance Edition)
Craftsmanship is something we can all get behind, because it essentially means creating beautiful code. And beautiful can mean easy to read, easy to extend/modify, easy to test, or all of the above (and more). And it’s fun to try new techniques and master patterns that allow our code to be beautiful.
For me personally though, I wanted more and C++ has delivered that in the form of performance. By that I mean that there are a whole new class of problems to solve for that, in my opinion, fall under the umbrella of crafting beautiful code.
When dealing with non-garbage collected data, we need to ensure that we maintain safe code. This means not destructing things that are in-use, not using things that have been destructed, and also not holding onto objects indefinitely (leaking memory).
This means making use of smart pointers to determine what is uniquely owned, what is shared, what is cached, and defining an ownership model within your application. This allows you to use smart-pointer types to explicitly state the memory-contract with other portions of your code.
- Is this piece of data guaranteed to exist as long as I have it?
- Can I keep a copy of this pointer for myself?
- Can I share copies of this pointer with other parts of the application?
In our garbage collected languages we rarely think of ownership, although we may think in terms of immutability, but that typically means copies. When a driving factor for our code is performance we trade copies for strong ownership semantics (when possible).
Imagine applying functional code to a list of data — map, flatMap, filter, etc. Now imagine that when you compile to this code, you get a tight-loop with zero unnecessary copies of your data with all those actions neatly collapsed. This, in essence, is a zero-cost abstraction.
It’s the ability to design libraries and write code that allows us to be expressive and write readable code. We then allow the compiler to optimize this high-level code into very efficient low-level code.
When writing C++, this is a core-principal of the language and standard library, and is something to aim for when developing internal abstractions and libraries.
If you want to go fast, you have to be careful and considerate with your concurrency patterns. Is your application I/O bound? Is it CPU bound? And have you modeled your application to take advantage of this limit?
If you’re I/O bound are you using an evented model like libevent or libev?If you’re CPU bound are you pinning your threads and utilizing NUMA? Have you correctly modeled your concurrency to maximize your applications performance?
Unlike higher-level languages, C++ allows you the freedom to finely tune how your application achieves concurrency. Giving you greater control over your applications performance characteristics.
Writing fast code relies on understanding how the hardware is going to execute your code and how it’s going to cache your application’s data. Organizing your code into patterns that allow you to utilize more cache and avoid cache-misses will be important to going fast.
Tight-loops are the canonical example. This means executing a loop that only involves local data (such as the list being iterated over). Another emerging pattern is called Data-Oriented Design which is a great way to maximize cache-efficiency at the application-level — a much larger scope of the tight-loop example.
Do You Want To Go Fast?
If this excites you, check out this awesome list of resources to get started learning C++ and going fast.