3 ms·
Isn't the ternary operator a branch? Could you just write (u8)(x <= y)?
by WithinReason 4y ago
Isn't the ternary operator a branch? Could you just write (u8)(x <= y)?
- dundarious 4y agoIn x86_64, a branch is typically considered to be a comparison and then a jump to different code (otherwise fall through to following code). Test sets some control flags, and the specific jump type jumps based on the value of a specific control flag. You might have something like: mov rax,rbx test rcx,rcx jnz L1 mov rax,rdx L1: ret A cmov instruction is a conditional move. It says, based on specific control flags, do a mov, otherwise do nothing. mov rax,rbx test rcx,rcx cmovz rax,rdx ret The conditional move is not really branching in the classic meaning of the word, because there is no jump (the instruction pointer unconditionally moves forward to the next instruction). Branches, even well-predicted ones, can hurt performance, especially if the jumped to code is not in cache (needs to be fetched). More detail can be found in this nice top-to-bottom video from Casey Muratori, which goes over modern CPU architecture (it's nothing too fancy, I think every person doing optimization should know this stuff): https://www.youtube.com/watch?v=8VakkEFOiJc https://www.youtube.com/watch?v=8VakkEFOiJc You can imagine the compiler generating either assembly from if style code or from ternary style code. Writing the arithmetic ternary style just happens to nudge the compiler into using cmov-s more often than other ways. For simple cases, a smart enough compiler could generate cmov-s from the if-else case as well, but it's not smart enough for even toy examples. Never mind that cmov-s are not unconditionally better -- a well-trained branch predictor is faster. The question is if your data is conducive to that or not -- something the compiler may never infer. So we're left to encode such information very indirectly via the code patterns, or via all sorts of __builtin_expect style intrinsics. This all goes to show that C is no longer portable assembler, and there are all sorts of crucial resources on the CPU (cmov, SIMD, etc.) that may go unused based on slight differences in the C code (e.g., even little things that might seem correct, like using unsigned for an accumulator, can ruin its ability to generate cmov-s). Compilers are really good at turning code that multiplies by a constant into a weird mov and lea that's cheaper than a mul, but they're still very spotty on everything else, and "everything else" covers a huge amount of the stuff that has made CPUs faster in the last 15+ years. Understanding your specific platform target(s) is still crucial if you want to do better than just "get lucky with the compiler sometimes".