As always, profile first so you don't fall into the premature optimization hole.
Sometimes you can guess whether the data the loop will hit will be predictable or not. The same applies; if you expect it to be predictable, then don't try to remove branches. If you expect it to be unpredictable, try to remove branches.
Note that you will need to teach yourself your expectations; the branch predictor might be better or worse than you expect it to be. Modern branch predictors are extremely complex, often taking more die space than everything except cache. Don't assume that just because you can't see a pattern in the data, the branch predictor won't be able to.
Exception would be a branch that's near impossible to predict, like one that depends on a randomly generated value or otherwise doesn't correlate well with global history.
Branch hoisting, on the other hand, is a win like 90% or more of the time (I can imagine it being a loss if it causes stuff to fall out of icache), which is why most compilers will try to do it automatically.
Worse than branches (generally) are indirect calls (i.e. virtual methods, function pointers) that don't predict well, so you especially want to avoid those in tight loops and if you can't avoid them you want to sort your data so that the call target doesn't change frequently.
I’d say that’s something you should only do once you understand your architectural bottlenecks. These are bottlenecks that appear because of how you’re doing computation and moving data around. They won’t show up on any profiler because the profiler is telling you “this is where an implementation is slowest” and not “there’s a better implementation altogether” - the latter is art as it requires a good working understanding of computer architecture and playing around with algorithms that aren’t taught in text books (or adapting them more optimally for your problem domain).
If you’ve done all other major architectural optimizations and you’ve identified a hot loop where the compiler is inserting branch code which is mispredicted, an explicitly branchless version will help. But remember - compilers are very good often at recognizing the opportunity for a branchless version and changing your code to that, so if you’re in AOT land with a major compiler (clang/llvm, gcc, msvc, Intel) there’s a good chance your algorithm might already be branchless. In fact, if you use something like PGO on a realistic workload, the compiler is even more likely to emit the correct machine code in way more places without you having to manually alter code by hand.
More impactful low level optimizations often are:
Optimizing for cache locality through something like entity component systems (ECS). Basically using a struct of arrays and no polymorphism instead of an array of polymorphic structs. This is a popular one for games and works really well. Similarly, make sure that data being accessed in a hot path is all linearly laid out in processing order if possible.
Applying SIMD to your hot loop processing. The compiler does have autovectorization that can work well with ECS but requires you to write your scalar code carefully to trigger it (ie your scalar code has to look similar to the vector version in important ways). Check the assembly to see if the compiler is doing it and rewrite using compiler intrinsics when it’s not. Similarly try to minimize data dependencies between your loops. Eg instead of
sum=0
foreach x in arr
sun += x
Try sum0 = 0, sum1 =0, … sum7 = 0
foreach (x0, x1, …, x7) in arr
sum0 += x0
…
sum = sum0 + sum1 +…
That’s very very similar to what a SIMD loop would look like and a compiler is likely to recognize it. Even if it doesn’t autovectorize, you’re now doing 8 summations “concurrently” because there’s no data dependency for those 8 additions on every loop iteration, so the CPU will execute them without waiting for the result of any other summation. Make it unrolled enough to exhaust the execution units and you’ve guaranteed that the beginning of the loop will start after the previous data dependency has been resolved (ie the CPU will be executing that loop at maximum speed). Again, this kind of stuff is silly to worry about until you figure out where your hot loop is for a given implementation.Knute writes “premature optimization is the root of all evil”. Implementation optimizations can get you very small improvements once you’ve exhausted low hanging fruit (basically implementation mistakes). Switching implementations can often net you order of magnitude improvements.
Looking into branchless is probably often premature. Organizing your data efficiently and using good algorithms is not. Data dependencies in loops is probably in the middle there - they optimize your current implementation but the improvement for that hot loop can be drastically more significant than a branchless design (fixing data dependencies can easily 10x your performance, making branchless for a mispredicted loop can net you maybe 20% or so for that hot loop).
For gamedev, where numerical correctness is often secondary to speed, I would expect to use `-O3 -ffast-math`. In games where deterministic simulation is important, (eg, multiplayer) you may need to use only `-O3` in the physics simulation and only use `-ffast-math` in the graphics portion.
while (money > 0) {
if (iOS) {
// ios revenue
money += 5;
// ios costs
money -= 2;
} else if (android) {
// android revenue
money += 1;
// android costs
money -= 5;
}
}
Anyway, to your real question, an entity component system-based middleware, hopefully in a language that isn't C++, forces you to author your code in a way where branches can be "moved" to remove the problematic memory access and execution pattern. So maybe it's not about specific C++ method branches, which may be beneficial to your employer but never beneficial to you or your career, but it's about the architecture of the thing that still has innovation opportunities that could really matter.You probably aren't interested in the even bigger picture conversation, which is that some games are so ill specified that it is impracticable to use an ECS middleware. Games that are really well specified (i.e. clones & sequels) might thrive on ECS, but the ones we end up playing are authored by companies with real budgets and take years to develop, so the compute platform will get faster than your micro-optimization.