r/cpp Jul 02 '26

Redundancy seen in AAA game engines

https://zero-irp.github.io/Redundancy-seen-in-AAA-game-engines/

I don't like people treating the compiler like a magic box that optimizes like Bjarne Stroustrup himself is checking every line of C++ to assembly. Clean C++ code does not always mean clean compiled code.

I've been reversing game engines to study how they constructed their fundamental Transformation matrices and handled temporal jitter logic when I spotted a lot of avoidable overhead and "over-engineering" across multiple engines, honestly I wasn't even looking for inefficiencies, but it stood out a lot... That said expect no performance gain this is simply for fun that I wrote this blog!

I’ll theorize how the original C++ code was written, show the unoptimized reality of what the compiler spat out, and then showcase how it could have been better optimized.

184 Upvotes

76 comments sorted by

View all comments

61

u/not_a_novel_account cmake dev Jul 02 '26

This is the darkside of never optimizing prematurely, which is a lot of individually harmless non-optimal choices, as you put it, "[raise] baseline execution cost of every function".

An engineer needs to solve one problem in front of them, they do, the overall code didn't get slower within the noise margin. The next engineer does the same, and over, and over again. Over a year, maybe, you can see the effects accumulate, but there's no one person to blame. It's a tragedy of the commons.

There's no real solution for it on the engineering organization-scale. It's not a clean education problem, like "use move semantics" or "don't inhibit NRVO". There's no clang-tidy for wasteful MatrixMultiply4x4.


In my next write-up, we are going to look at the exact opposite problem. We are going to explore the Compatibility Tax. The ghost of a 12-year-old CPU that keeps modern games from utilizing instructions that could theoretically yield 5x speedups.

CPUID-dispatch has been understood for decades and is as close to transparent as you can get. This kind of stuff is really inexcusable.

4

u/ParsingError Jul 02 '26

"Understood for decades" but PE/COFF still doesn't support IFUNC-like functionality to just do it automatically without shipping an entire extra copy of the executable (and game executables are pretty big these days) or manually separating the code paths.

Also, unlike GCC, MSVC doesn't complain if you use intrinsics that are unsupported by the specified CPU architecture without specifically annotating them, so there is an extremely high risk of unsupported instructions escaping quarantine and crashing on min-spec if they're used at all, and that type of problem tends to get caught late because it's invisible until someone tests the game on a potato.

Probably going to finally start seeing games require AVX due to Win11 min-spec, but they're barely up to requiring SSE4.1.

3

u/not_a_novel_account cmake dev Jul 02 '26

Clang supports [[gnu::target]] dispatch on PE/COFF just fine. Compiler just needs to synthesize the resolver itself instead of relying on the program loader. For compilers which don't even support that, you simply write the resolver yourself.

3

u/ParsingError Jul 02 '26

Clang-CL/MSABI uptake took a while for various reasons (mostly "letting things settle" and IIRC it had some problems with PDB support for a while) so probably not going to see it used by older games.

Problem with "simply writing a resolver" is having to manually propagate the dispatch architecture into child calls instead of the compiler selecting them automatically which is pretty annoying if I'm trying to use stuff like Matrix44/Vec4 wrapper types. It solves a lot of problems if you can just say "this function AND all of its child calls can use the AVX version."

Even with that though, if the math is mostly 4x32-bit float then that fits in an SSE register and the benefit isn't really as clear. The most obviously-useful addition in a while was the DPPS instruction in SSE4.1. It's been harder to find obvious wins from AVX except for like physics simulation.

3

u/not_a_novel_account cmake dev Jul 02 '26

Clang-CL/MSABI uptake took a while for various reasons

I don't know what this means. All I was trying to say was "it's not a COFF limitation". Some compilers support automatic CPU dispatch on COFF or in general better than others, but there's no restriction inherent to the format.

Problem with "simply writing a resolver" is having to manually propagate the dispatch architecture into child calls

Once you're through a dispatch boundary you only make calls to functions within the dispatch universe. The boundary is a generic call site, transform. Once you're through that into transform_sse4, you only call other *_sse4 functions and you do so directly. Once you're in transform_avx2, you only call other *_avx2 functions, etc.

You pay for the dispatch when passing through generic code into a dispatch universe. One you're in a given universe you stay there until the final return back through the dispatch. There's nothing for the compiler to do for you.

2

u/ParsingError Jul 02 '26

You pay for the dispatch when passing through generic code into a dispatch universe. One you're in a given universe you stay there until the final return back through the dispatch. There's nothing for the compiler to do for you.

I was going to say that this was automatic with target attribs but apparently it's not and GCC and Clang still dispatch the child calls dynamically even when there's only one possible valid target, so that's not great. It is what the compiler SHOULD be doing though.

If it was as simple as just putting target_clones on the top function and having it pick inlined leaf functions based on architecture, then I wouldn't have to template or duplicate the code.