2026-07-09
Modern SIMD ISAs (AVX-512, SVE, RVV) let you attach a mask register to a vector instruction so lanes with a zero mask bit don't commit their result. On paper this is beautiful: no branches, no scatter/gather gymnastics, just "do the math and throw away what you don't need." In practice, masked lanes are one of the sneakiest performance cliffs in vector code.
The problem starts at the execution unit. A masked FMA still flows through the multiplier, adder, and rounding logic for every lane — the mask only gates the writeback. Power and latency are paid in full. Worse, the scheduler must treat the mask register as an extra input operand: the instruction cannot wake up until the mask is ready, adding a dependency edge that unmasked code doesn't have. On Intel's AVX-512 implementations, this shows up as a 1-cycle extra latency for mask-dependent ops, and a real bottleneck on port 5, which handles most mask-register manipulation (kmov, kand, kor).
Then there's merge-masking vs. zero-masking. Zero-masking ({z}) is cheap: masked lanes become 0, no old value needed. Merge-masking requires the previous value of the destination register to preserve masked lanes — which means the destination becomes an input. Register renaming can't eliminate the false dependency, so a loop like:
vaddps zmm0{k1}, zmm1, zmm2 — each iteration depends on the previous zmm0, serializing what looked like independent adds.The classic real-world example: a numerical kernel iterating over a padded array where the tail is masked off. Naive merge-masking on the tail iteration serializes it against the previous full-width iteration, turning a 4-wide unrolled loop into a chain. Fix: use zero-masking on the tail, or split the tail into scalar cleanup.
Rule of thumb: a masked SIMD op costs roughly (unmasked latency) + 1 cycle for the mask dependency, plus a false dependency on the destination if merge-masked. If your mask is computed from a compare 3 cycles upstream, the whole vector op stalls those 3 cycles even if all other operands were ready ten cycles ago.
Also worth knowing: on some microarchitectures, an all-zeros mask does not skip execution — the unit still fires, still burns power, still occupies a port cycle. AMD's Zen 4 partially optimizes this ("zeroing idiom detection"), Intel largely doesn't. Profiling with UOPS_DISPATCHED.PORT_5 will expose mask-register port pressure faster than any code review.
