select_naive: a BRANCH, on the pointer, not a cmov:
cmpl $0, (%rdx,%r9,4)
movq %rsi, %rcx
je .LBB0_8
movq %rdi, %rcx # rcx = c[i] ? a : b ... then load (%rcx,%r9,4)
Clang chose to branch and load only one side. Reasonable in general
(loading both sides could touch memory you never needed), fatal on
random data: 7.3 ns/elem = a mispredict every other element.
select_cmov: loading both sides first made the select if-convertible,
and then VECTORIZABLE. There is no cmov at all: a 128-bit blend:
movq (%rdi,%rax,4),%xmm3 ; movq (%rsi,%rax,4),%xmm5 ; movq (%rdx,%rax,4),%xmm7
pcmpeqd %xmm1,%xmm7 ; pand %xmm7,%xmm5 ; pandn %xmm3,%xmm7 ; por %xmm5,%xmm7
Four elements per step, no control flow, 1.0-1.5 ns on random data:
5-7x faster than the branch. Same source semantics; different shape.
select_branch: the asm barrier keeps a real jcc; it tracks `naive`.
compact: branchless by construction, and clang found a neat trick:
movl (%rsi,%r8,4), %r10d ; movl %r10d, (%rdi,%rax,4) # unconditional store
cmpl $1, (%rdx,%r8,4) ; sbbq $-1, %rax # j += (keep != 0) via carry
The store always happens (to out[j], overwritten next time if not
kept) and the index advances via subtract-with-borrow. ~1 ns/elem
regardless of the data. This is how you write a filter for
unpredictable predicates.