Search code examples
cgccassemblyclangx86-64

Is an extra move somehow faster when doing division-by-multiplication?


Consider this function:

unsigned long f(unsigned long x) {
    return x / 7;
}

With -O3, Clang turns the division into a multiplication, as expected:

f:                                      # @f
        movabs  rcx, 2635249153387078803
        mov     rax, rdi
        mul     rcx
        sub     rdi, rdx
        shr     rdi
        lea     rax, [rdi + rdx]
        shr     rax, 2
        ret

GCC does basically the same thing, except for using rdx where Clang uses rcx. But they both appear to be doing an extra move. Why not this instead?

f:
        movabs  rax, 2635249153387078803
        mul     rdi
        sub     rdi, rdx
        shr     rdi
        lea     rax, [rdi + rdx]
        shr     rax, 2
        ret

In particular, they both put the numerator in rax, but by putting the magic number there instead, you avoid having to move the numerator at all. If this is actually better, I'm surprised that neither GCC nor Clang do it this way, since it feels so obvious. Is there some microarchitectural reason that their way is actually faster than my way?

Godbolt link.


Solution

  • This very much looks like a missed optimization by both gcc and clang; no benefit to that extra mov.

    If it's not already reported, GCC and LLVM both accept missed-optimization bug reports: https://bugs.llvm.org/ and https://gcc.gnu.org/bugzilla/. For GCC there's even a bug tag "missed-optimization".


    Wasted mov instructions are unfortunately not rare, especially when looking at tiny functions where the input / output regs are nailed down the calling convention, not up to the register allocator. The do still happen in loops sometimes, like doing a bunch of extra work each iteration so everything is in the right places for the code that runs once after a loop. /facepalm.

    Zero-latency mov (mov-elimination) helps reduce the cost of such missed optimizations (and cases where mov isn't avoidable), but it still takes a front-end uop so it's pretty much strictly worse. (Except by chance where it helps alignment of something later, but if that's the reason then a nop would have been as good).

    And it takes up space in the ROB, reducing how far ahead out-of-order exec can see past a cache miss or other stall. mov is never truly free, only the execution-unit and latency part is eliminated - Can x86's MOV really be "free"? Why can't I reproduce this at all?


    My total guess about compiler internals:

    Probably gcc/clang's internal machinery need to learn that this division pattern is commutative and can take the input value in some other register and put the constant in RAX.

    In a loop they'd want the constant in some other register so they could reuse it, but hopefully the compiler could still figure that out for cases where it's useful.