2

While using Godbolt Compiler, I was playing around with various functions when I checked what signed division looks like using the following code:

int div10(int num) {
    return num / 10;
}

Where the num/10 section generated the following assembly (RBP-4 is storing num at this point):

        mov     eax, DWORD PTR [rbp-4]
        movsx   rdx, eax
        imul    rdx, rdx, 1717986919
        shr     rdx, 32
        sar     edx, 2
        sar     eax, 31
        mov     ecx, eax
        mov     eax, edx
        sub     eax, ecx

I am aware that this is a typical example of division by reciprocal multiplication, using a fixed-point representation of 1/10 and bit shifts. What I do not understand is the choice of the constant 1717986919, or 0x6666_6667 in place of the typically cited (at least in Agner Fog's manual, https://www.agner.org/optimize/optimizing_assembly.pdf on page 138) constant 0xCCCC_CCCD. Note that the former is just the latter shifted to the left by one.

I can only assume that this has something to do with it being signed multiplication instead of unsigned multiplication, as the code generated when the type is uint32_t is as follows:

        mov     eax, DWORD PTR [rbp-4]
        mov     edx, eax
        mov     eax, 3435973837
        imul    rax, rdx
        shr     rax, 32
        shr     eax, 3

Where 343597387 is the expected value 0xCCCC_CCCD.

I also do not fully understand the shifting and subtraction that occurs during the signed division example. The first two signed right-shifts are the standard discarding of the decimal (fractional? I do not know the proper terminology for this) bits. However, I do not know why eax, which as a reminder holds the original dividend, has its sign "extracted" (the sign bit is made to fill all the bits with an arithmetic shift) and then subtracted from the quotient. (Register shuffling occurs to force the desired output into eax since the return value must go in that register before the ret instruction executes.)

Sidenote: Unless Agner Fog has published new manuals, I could not find any mention of signed division in his book.

Peter Cordes
  • 328,167
  • 45
  • 605
  • 847
Dory L.
  • 133
  • 8
  • 5
    IIRC, without the fixup of subtracting `eax>>31` (0 or -1), i.e. adding 1 with negative inputs, the negative result would be rounded toward -Inf (floor) like a shift, instead of truncated toward 0 like integer division in C and like the `idiv` instruction. – Peter Cordes Feb 10 '23 at 02:57
  • it's because arithmetic shift rounds towards negative inf, not 0: [Is there any similarity between the results of an unsigned and a signed division?](https://stackoverflow.com/q/39148333/995714), [performance of unsigned vs signed integers](https://stackoverflow.com/q/4712315/995714). The same thing happens for powers of 2: [Why is such complex code emitted for dividing a signed integer by a power of two?](https://stackoverflow.com/q/12692111/995714) – phuclv Feb 12 '23 at 09:50
  • Does this answer your question? [Why A / is faster when A is unsigned vs signed?](https://stackoverflow.com/questions/49477554/why-a-constant-int-is-faster-when-a-is-unsigned-vs-signed) – phuclv Feb 12 '23 at 09:50
  • 1
    I think I understand the final set of operations (rounding to 0 instead of neg infinity) with these links, but I still don't understand why the reciprocal constant was shifted 1 bit right in the first place. – Dory L. Feb 12 '23 at 17:18

0 Answers0