Writing Efficient C++ Code (2013)
asawicki.info
asawicki.info
Beyond the low-hanging fruit like ensuring you aren't creating O(n^2) complexity by accident, I think C++ is fast enough/has mature-enough compilers that by the time you're worrying about cache hits materially affecting performance, you're probably also sufficiently staffed and capitalized to pay people to A/B test that performance.
More than having done it a few times though, what is more important is to have thrived in a space that is welcoming of intellectual curiosity and play in matters of performance. That's how one becomes good at it.
I think it's more like: prioritize cache locality over big O compexity.
2. Virtuals are, with the exception of PGO, mostly a black box i.e. you get a hard optimisation boundary, no inlining at all.
3. The C++ standard library is usually comically slow (yes, even compared to Java/C#/the likes) so if your project uses std::vector and the such instead of specialised libraries, you've already lost at the beginning.
4. If you don't pay attention to performance from the get-go, the approximate amount of autovectorisation you'll get is close to zero. Some compilers are better than others (Clang>MSVC for example) but I've seen codebases with 8 figures of LoC where the number of vectorised divides/multiplys was like less than ten when you dumped the object listing. In the whole program.
5. Since aliasing and other optimisation barriers (you didn't use restrict or manually hoist, did ya?), it's not uncommon for large C++ programs to spend a third of their runtime doing atomic increments because shared_ptr is supposedly cheap and who cares about lifetimes anyway.
6. If you're targeting Windows, the default new operator / malloc is also comically slow. Luckily that one is fairly easy to fix with installing mimalloc and deploying the hijack dll, but the negative effects on cache by the fragmented allocations is also significant.
Surely nobody outside microsoft is doing serious work targeting Windows any more are they? Isn't that a dead platform? I read somewhere a while back they're now below 60% market share.
std::vector doesn't have trivial relocation so any type with a destructor ends up doing elementwise destruct+construct instead of a memcpy.
std::map and std::list are memes and if you use them you're giving your CPU the 1995 treatment with all that pointer chasing.
You thought std::unordered_map is better? Well, actually not because node stability, so it's still chained-bucket, you almost always want to use a flat map like boost::unordered_flat_map or the abseil/eastl version.
<random> is hard-to-use and isn't very performant, std::regex is "you might as well write it in Python and it'd be faster", <iostreams> is virtual calls galore, both the formatting and the stdio functionality are slow.
The conveniently-named std::function is a very general device resulting in a heap allocation and usually a virtual call, there's specific optimisations but don't rely on it.
The STL string manipulation functions are also usually slow, they check the locale for string manipulation rules.
The floating-point functions set errno preventing vectorisation and emitting branches in your straight-line float code unless you use fastmath (the thing people tell you never to do) or one of the more fine-grained compiler-specific switches to turn it off.
std::shared_ptr is Arc<T>, not Rc<T> and eating the cost of atomics can add up in many situations especially with all the other memory traffic going on.
std::variant and std::visit are also not very fast either.
std::filesystem as a whole also has several pain points like iteration which is like a magnitude slower than the native APIs, std::chrono isn't much better either
std::error_code sounds like a simple integer or even a struct.... lol no guess what, more virtual calls
2. A non-inlined grow. If you have large collections you modify often, you want a vector implementation where the reallocation is out of line and the rare case. All the STLs treat it as a normal method and have inlined by codegen.
3. Trivial relocation support so you don't need to destruct objects where there are no pointers inside or external objects pointing to them.
In your case it's probably not as relevant/important, yes
As a contrived example: there are specific cases when a particular non-quicksort algorithm is optimal. In almost all real world scenarios, though, you're just going to say fuck it and use quicksort until profiling determines that the sort is the bottleneck.
Unless you already have specific knowledge that your data comes in a particular shape, defaulting to quicksort is good design (IMHO). Worrying about pathological sorting before you've seen benchmarks is premature optimization.
I work in game development and for the last six years I've spent most of my time specifically on optimization. A lot of that effort has been focused on cache behaviors. Not because it's fun, but because it's often the difference between being able to ship the game on weaker hardware (e.g. Nintendo Switch) or not.
Yeah, it's really not.
There are multiple areas of work, where C++ can be considered a glue language. The high-performance work is then done in explicit SIMD (intrinsics, ISPC, etc.) and/or GPU-targeting languages such as CUDA or Vulkan.
In these areas of work, high performance is part of the design and not something that can be easily added as after-thought.
Also, relying on optimization features such as compiler auto-vectorization is way too finicky - your hot-loop performance may completely break without anyone noticing by someone changing a trivial-looking part of a loop.
Run your own benchmarks on your own data of course. Also map is not considered the best key value store.
This likely won't be true in a real application with a non-trivial allocation pattern.
And then, on the other hand - I really doubt GP's map beats a vector, with all of those pointers bins and stuff, in a non-contrived benchmark with 10 elements.
Finally - it's not either-or: There are better hash maps whose memory is sequentially allocated and/or are otherwise cache-aware. And there are data structures geared towards parallel execution on multiple threads; and towards SIMD; etc. etc.
Still a custom map that allocated a bunch of nodes would be a useful optimization.
If you’re down to that sort of decision-making, you have to measure.
The growable array type std::vector<T> is least impacted by these archaic choices out of the tools in the box you're likely to reach for. So it will make sense very often to choose this type first.
1. When you need a persistent version of sequential data structure. I.e. you need addition not to change the previous version of list. Very useful in traversals which can fail and/or have multiple routes. C++ list obviously fails here because it's a mutable data structure and each addition is mutation. The proper interface is cons(head, old_list) -> new_list, where old_list exists after new_list is constructed.
2. When you need the values be never moved in memory. Aka intrusive lists. Can be optimized for more cache friendliness by having lists of big chunks of values instead of just lists in some cases. Useful in operating systems and many low level apps. Alternative is usually a vector of pointers which still gives you indirection.In what way? It's the same thing with additional overhead on copying the vector when adding/removing elements.
Not to mention you can have a lock-free intrusive list, and with vector well, you just can't.
> Copying the whole vector is usually better
With linked list you don't need to copy anything when you add/remove.
https://www.boost.org/doc/libs/1_92_0/doc/html/container/non...
This is an array of pointers, I mentioned it in the post you're replying to. It completely obliterates the "cache-friendliness" argument, making it worse than linked list (now you have same indirection overhead plus overhead of copying minus benefits of being able to CAS your value atomically into a list making it lock-free)
I figure it would be possible to add support for deletions, but it would cost us: we would lose guaranteed contiguous placement of elements with neighbouring indices, and (unless no deletions are made) we'd need a private data structure to correspond vector indices to addresses, and to determine where to locate new elements. This would of course bring us back to continually paying the price of indirection overhead, and simple lock-free modifications would not be possible.
My completely unsupported guess is the cache behaviour wouldn't be too bad unless deletions (of elements that aren't at the end of the vector) are common. I imagine the cache behaviour of a linked list must depend greatly on what the allocator gives you. Presumably using a pool, specific to that particular list, could help there.
The Linux kernel uses them, at least some of the time they're used with their lock-free RCU pattern. I'm not sure if it's for performance reasons though, I think they're using it in contexts where correctness requires the absence of blocking operations.
I'd expect a lock-free non-linked-list solution would also be possible, but I don't know enough to state that definitively.
My guess for the 0.x releases in particular is that there's a lot of the latter and as Linux goes from "Like Minix but I made it in my bedroom" to Serious Business™ more and more of the former.
In the early releases of Linux, the cache locality argument wasn't as prominent an issue on the hardware of the day. So the computer science textbook argument of O(1) inserts and [if you have the node pointer already] removals was more compelling.
Or are pools used to avoid that?
Well not explicitly, but it uses a version of malloc that has a pool for every rounded object size.
But your question reminded me of another aspect of linked lists in the Linux kernel: unlike a lot of high level languages, there isn't an extra allocation for a node structure. The node structure is a member of the structure being linked.
Often the structure being linked might be something like a reference counted heap object, so the question of adding an extra member to store the next pointer is not a big difference.
That's good, but it seems like how-hanging fruit. Boost offers intrusive_prt for this. [0]
make_shared goes half way, and performs a single allocation to return a shared_ptr to a new object. It eventually made its way from Boost to the standard. [1][2]
See also [3] which contrasts the two. (As you can imagine, intrusive_prt is slightly more efficient.)
[0] https://www.boost.org/doc/libs/latest/libs/smart_ptr/doc/htm...
[1] https://www.boost.org/doc/libs/latest/libs/smart_ptr/doc/htm...
[2] https://en.cppreference.com/cpp/memory/shared_ptr/make_share...
He actually cited this use case, of a structure that has list or tree nodes inline with the data type, as a strength of the model.
You can also take independent modules that provide their own statically allocated memory and chain them together using the reserved linked list cells. (think kernel modules)
This is a bit of a wishy washy explanation because I work on a highly adjacent project that has similar constraints but I never looked at the kernel source (strictly working with statically allocated memory during startup).
This is wildly overstating it. Yeah, I agree, they're much less often the right choice compared to a good-ol' growable array, but they have lots of uses in high-performance code and concurrent code, and they're building blocks in lots of other data structures. Like, in a bucket hash-table, the buckets are linked lists, in a LRU cache you interleave a hash table and linked list, std::hive is a linked list of chunks of elements, etc. Anything that has ever had to deal with memory pooling/allocation uses free-lists which are linked lists. And on and on and on.
The C++ 23 containers are: array, vector, deque, forward_list, list, set, map, multiset, multimap, unordered_set, unordered_map, unordered_multiset, unordered_multimap
Rust's collections are: BTreeMap, BTreeSet, BinaryHeap, HashMap, HashSet, Vec, VecDeque
Firstly, Rust doesn't consider "array" a library type here, in C++ the language has built-in arrays but they're very poor because they are the C arrays - so you use the library feature to get good arrays. In Rust they... just fixed the language, because duh.
Next thing you'll notice is that C++ has lots more of these types, about twice as many. I stopped at C++ 23 because in C++ 26 they added even more. These are a significant maintenance burden and of course having more means in practice maintenance gets worse. But this could be good if these types were all high quality and kept that way.
All of the C++ unordered containers are the same crap hash table design but with slightly different parameters. The Rust HashMap and HashSet are Swiss Tables though they do not promise that and if a better design comes along they will probably switch. C++ can't change the design because the API welds them to a very specific shape for this data structure, a shape which delivers bad performance on any vaguely modern hardware.
std::deque is the most horrible surprise. A modern programmer who has thought about it at all is expecting a type like Rust's VecDeque. Generalise the amortized growable array from the language to use it as a ring buffer. Cheap push & pop at both ends, canonically use it as a FIFO but also practical in lots of other situations. But that's not what std::deque is at all, instead inside it's an array of links to small arrays. On MSVC it's effectively a linked list again because those inner arrays contain only one item due to ABI considerations.
std::set and std::map are very principled red-black trees. I say principled because in practice this is too expensive on modern hardware because (say it with me) it spends too long chasing pointers up and down your tree. Rust's choice here in BTreeMap and BTreeSet packs more data in each "node" on the tree, which makes the big-O worse but the practical performance better. Figuring out how to best do this for the general case is an active area of research but "I bet a pure red-black tree will be fast" is not a good guess for the past several decades.
Finally std::forward_list and std::list are the singly and doubly extrusive linked list types. The thing you most likely have seen in some high performance software is an intrusive linked list, and C++ doesn't provide those. In an intrusive linked list each item in the list itself links to where the next (and for simple double links also the previous) item is, so the item needs to know it's in a list [in some systems more than one list, thus more than one set of links]. C++ provides extrusive linked lists where those links live in a separate object and so the items in the list don't know about this at all. Rust provides only a doubly-linked extrusive list exactly like C++ std::list, but again, this almost certainly isn't what you wanted, you most likely do not need a linked list and if you do have a good reason for a linked list you probably want an intrusive linked list.
For example, since I allowed for objects to be shared between threads, I decided to use struct of arrays so the reference count, metadata, and value would be stored in separate cache lines. This ended up hurting me because object initialization touched three separate cache lines (obvious in hindsight, but the advice of using SoA failed me here). I also heard that you want to pack your values as tight as possible, so I used a packed string index, but then I ended up with integer division to unpack the string (also a mistake, but again the advice failed me). I used a custom allocator to avoid indirection with lists (list items were allocated directly after the list head), but then I had heap fragmentation and the implementation complexity exploded.
Anyways, I am now happily using two to three levels of indirection in my data structures, large structs, and malloc for individual objects, and it's still been faster in my end to end testing. So maybe this is unique to interpreters, and maybe I could have done it better, but the suggestions don't automatically apply in my experience.
I mean this is kind of what happens with any advice that has nuance to it, that's not carried with the advice.
E.g. if you have a point in 3D space with x, y, z coordinates. Array points as SoA of individual dimensions makes sense only if you do a lot of averaging and such on the individual dimensions.
If you mostly use the 3 coordinates together, SoA will have bad caching behavior.
So the better advice would be to try to keep things that are used together in the same cache line, whether it's on dimension or all 3. Usage makes the difference.
I'd say the rule was followed in this case - the rationale of SoA is to reduce cache misses when iterating all objects and only using some of the attributes, which is something games do all the time, but it's bad if you are always accessing one object at a time. Maybe an array language interpreter would have luck with SoA.
I feel like the DoD movement is a slow-moving, but big, change through how systems programming is done, but that there's still insufficient material for how to do this in different scenarios. I would really like to apply this more to my areas of work, which are also in C++, but there seems to be a gap between what they're presenting and how it can be applied.
More specifically, I'm using C++ to build a dynamic programming language runtime for a Clojure dialect. That runtime is required to be garbage collected, type-erased, and highly polymorphic. So I surely can't just SoA or AoS everything. Yes, I can pack my data, and I can avoid the GC whenever possible, both in compiler/runtime code and in generated code via escape analysis. But what about everything else, which is the 80% or more of the system? It could be that this runtime is too far at odds with DoD, but I generally see things as a gradient rather than black and white.
Or were you referring more to all the intermediate allocations that aren't the object heap? V8's zones are interesting in this area, because they're like an arena, except that they're only partially reset when a zone ends, so zones can nest inside each other.
High-performance programming is a big topic. The scope is far too broad for a single blog post, which naturally gives only cursory discussion of C++ and computer architecture. The article isn't bad considering, but I do think it's the wrong format. A blog series, or even a book, would be more fitting.
(I'm not creata, but I imagine this is what they had in mind.)
What you've written mostly makes sense to someone who already has a solid understanding of SIMD and of C++ (although I can't say I follow all of it), but the target audience is people who don't. For them, each point needs a much lengthier explanation.
A quick restrict example:
#define fn __attribute__((used))
fn void copy1(int* to, const int* from, const int size)
{
for(int i = 0; i < size; i++)
to[i] = from[i];
}
fn void copy2(int* to, const int* from)
{
constexpr int size = 1024;
for(int i = 0; i < size; i++)
to[i] = from[i];
}
fn void copy3(int* restrict to, const int* restrict from)
{
constexpr int size = 1024;
for(int i = 0; i < size; i++)
to[i] = from[i];
}
gcc test.c -c -O3 && objdump -d ./test.o
copy1 is 52 lines, copy2 is 28 lines, copy3 is 2 lines (just a call to memcpy).This is a good starting point for self teaching. The impact of your TLB, L1, and overall instruction count (with IPC) can further be measured with `./perf stat -d -d -d ./a.out`. If you want a quick rule of thumb, no instructions are fast instructions.
creata's comment [0] mentions the works of Agner Fog, which seem very good, and are freely available.
I haven't read C++ High Performance [1] but it looks like it covers the sorts of topics you'd expect, although it looks like it doesn't cover computer architecture in detail e.g. branch prediction. There are books on that too, of course.
[0] https://news.ycombinator.com/item?id=49868657
[1] https://www.packtpub.com/en-us/product/c-high-performance-97...
In short, overpromises, underdelivers.
If your device has enough resources to power V8, modern GUIs are certainly very pleasant and snappier than a more minimal GUI like HN. Otherwise they are horrendous and very laggy.
QtQuick/QML/JS is very pleasant and I do wish more people would use it, but from I've seen it's 25-50% the resource use of electron, not some multi-order-of-maginute improvement, do I understand why many people still prefer electron for portability in this case.
The next step of going SOA benefits from all of the above, it just further unlocks you packed quad and oct instructions (AVX256 and 512 depending if you buy AMD or not).
GCC 11 (2021) std::visit was slower than virtual dispatch.
GCC 12 (2022) optimized std::visit so it can be faster than virtual dispatch.
https://shubhankar-gambhir.github.io/posts/your-stdlib-imple...
Modern graphics APIs allow you to render as many objects as you want with a single draw indirect call.
Even if you write them in hand-optimized assembly they would still clamor for more speed.
The kicker is, in my case I chose C++ because templates allow me to reuse most of the code in the rendering pipeline _regardless_ of whether I go for AoS or SoA layout. I leverage operator overloading to do vector by matrix multiplication which is implemented in both variants. I do have to specify the desired variant during building, but I've profiled and for Intel x86 AVX in my case SoA is something like twice as efficient because I process ("shade") 8 vertices with 4-5 instructions instead of 1 vertex at a time (still shaded with vectorisation -- just "rotated", i.e in the pipeline axis and not vertex buffer axis).
TL;DR; C++ gives you plenty fast by default, but it's not always enough. The difference between 5 and 15 frames per second, well, makes all the difference -- our eyes are only fooled once the frames-per-second rate goes sufficiently up, anything below an acceptable threshold and it's completely different experience. You then either sacrifice resolution or level of detail etc, or decide to squeeze more from the language by helping the compiler.
https://youtu.be/jsdwRf3JvZM?si=0tysoCsvaWl0R_PZ
40’000 NPC in game with collision avoidance, steering, on 8yo hardware.
https://devblogs.microsoft.com/oldnewthing/20060731-15/?p=30...
https://learn.microsoft.com/en-us/archive/blogs/ricom/perfor...
"Just because I don't write about .NET doesn't mean that I don't like it"
"Performance Quiz #6 -- Chinese/English Dictionary reader"
https://web.archive.org/web/20250201145327/https://users.ece...
Note that we started our project before Rust was an option. These days I would certainly look at rust to see if that would cover our 5% of the needs but now we have a lot of C++ and mixing rust with C++ is a pain.
It is less pain than for most other languages, except for C. The pain is in exposing a C API for your C++ code. Then you build a library and you're set - because basically every language has the ability to call C code. Python, Rust, Java, etc. etc.
The painful part is to have to go through a C API (modern languages can express much richer APIs and of course there are different constraints on the different runtimes, e.g. GC).
The annoying part is that each language adds overhead (its runtime). I wouldn't call it painful (I don't have much to do about it), I say "annoying" just because I would rather minimise the amount of code I ship.
Pointers - better null check them even if you guarantee they won't be null. (assuming they are supported at all in the target, if not copy all the data in out)
Want to use a string - your language probably has a better string type the null terminated C string - are you going to pay the overhead to copy the string (remember to copy it back and forth for each call); or are you going to use C strings in your non-C code? Don't forget to remember the length of the buffer is different from the length of the string if you modify that string.
What to use a list - C supports arrays (which are just syntactic pointers). Nearly ever language has a better list type, which for starters encodes length.
Many languages are garbage collected, which is going to require a lot of work to make work well across the C API.
And so on.
For the above, as a C++ programmer I'm reaching for std::string and std::vector - which are both very good. I have a much better type than the C API allows, but I can't use it. Odds are your language has an equivalent that is just as good (possibly with different trade offs - those details are not the point so lets not argue it here), but since the memory layout isn't 100% the same you can't use my string/vectors directly in code as if they are the native types.
On the surface it seems easy. If you are only doing it a few times it isn't hard. However the details are hard and important and the more you mix the harder it gets since the above details (any many many more on the same lines) start to matter.
I agree that the more you mix, the harder it gets. Maybe I'm just never working on "serious" projects, but I have never been in a situation where I had to mix 10 languages.
Similarly I like to do video stuff in C just because I call gstreamer/ffmpeg directly in C, rather than having to bridge everything.
How 100Gbit NICs could your filter through your stateful firewall at line speed? And with 64 byte packets?
In (soft) realtime audio programming, your audio callback might only have a time budget of 1.3 milliseconds. Everytime you exceed that limit, you'll hear a dropout. That's when you'll start to optimize the hell out of your program :)
There are so many things that are expressible in C++ now that could not be without writing much more code or using per-compilation tools back then. The ability to run code at compile time that is not run at runtime is huge, #embed lets us make other tools output available without linker scripts or compiler specific tools that.
Also, most of the code from the past still works(from 10 years ago definitely works)
You've responded to that suggestion with seemingly irrelevant comments. Do you see why I'm confused?
For what it's worth, my actual opinion is that the committee is usually wrong/misguided.
Bjarne is often in disagreement with the committee
On one hand, you have consteval and stuff, letting you FINALLY initialize data at compile time (hey, 20 years late but still!)
on other hand, it is done in most non-debuggable way possible. try setting breakpoint or adding print to constexpr function that causes your requires clause to fail...
so no, newer C++ the language is not possible to use for low level work. The dialects that compiler makers support are. We will see for how long
This seems like an overstatement. Can you elaborate with some examples from your particular domain?
Or take constexpr - it permits to move computations to compile time that are complex and in older versions either had to be done at runtime, or an ugly workaround had to be used (e.g. assigning a mysterious literal pre-computed in another run or by hand).
What C++23 feature allows that?
[0]: https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2025/p27...
[1]: https://herbsutter.com/2025/11/10/trip-report-november-2025-...
So many complex, esoteric, and difficult to maintain incantations that used to be required for efficient code generation are no longer necessary.
I find it crazy that popular systems languages didn’t have an explicitly valid way to deal with all of these ambiguous ownership and lifetime issues around memory until relatively recently.
At the time I thought this would naturally fit in a data-oriented design/ECS system to run complex queries. I wonder whether anyone has tried this before and whether this actually works in practice?
Actually, we don't know that. The meaning of volatile is rather subtle
"Premature optimization is the root of all evil"
So, if you are thinking about the sort of “business logic” that’s often Python, but the performance comes from the parts that are usually not.
Just that Python culture has a strange way to call bindings to native code, "Python libraries".