4 ms·
TLDR: worry about branches in your hot loop kinda last and only if the profiler indicates you have a random walk through your branches AND you’ve verified the c
by vlovich123 3y ago
TLDR: worry about branches in your hot loop kinda last and only if the profiler indicates you have a random walk through your branches AND you’ve verified the compiler emitted branches. There are more impactful optimization techniques to worry about first and often with PGO and LTO the compiler is going to be a lot better about making relevant code branchless on your behalf without you having to think about it.
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).
- nwallin 3y agoNote that GCC will vectorize the first version if you add `-ftree-vectorize` to your optimization arguments. In order to vectorize the same loop with floats, (particularly relevant for gamedev) you need to add `-ftree-vectorize -fassociative-math -fno-signed-zeros -fno-trapping-math`. 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. https://godbolt.org/z/W3GTq5oo5 https://godbolt.org/z/W3GTq5oo5 https://godbolt.org/z/3qTnWrPsr https://godbolt.org/z/3qTnWrPsr