re implement modulo using bit shifts?

后端 未结 5 1547
情歌与酒
情歌与酒 2021-02-07 16:10

I\'m writing some code for a very limited system where the mod operator is very slow. In my code a modulo needs to be used about 180 times per second and I figured that removing

相关标签:
5条回答
  • 2021-02-07 16:30

    What you can do with simple bitwise operations is taking a power-of-two modulo(divisor) of the value(dividend) by AND'ing it with divisor-1. A few examples:

    unsigned int val = 123; // initial value
    unsigned int rem;
    
    rem = val & 0x3; // remainder after value is divided by 4. 
                     // Equivalent to 'val % 4'
    rem = val % 5;   // remainder after value is divided by 5.
                     // Because 5 isn't power of two, we can't simply AND it with 5-1(=4). 
    

    Why it works? Let's consider a bit pattern for the value 123 which is 1111011 and then the divisor 4, which has the bit pattern of 00000100. As we know by now, the divisor has to be power-of-two(as 4 is) and we need to decrement it by one(from 4 to 3 in decimal) which yields us the bit pattern 00000011. After we bitwise-AND both the original 123 and 3, the resulting bit pattern will be 00000011. That turns out to be 3 in decimal. The reason why we need a power-of-two divisor is that once we decrement them by one, we get all the less significant bits set to 1 and the rest are 0. Once we do the bitwise-AND, it 'cancels out' the more significant bits from the original value, and leaves us with simply the remainder of the original value divided by the divisor.

    However, applying something specific like this for arbitrary divisors is not going to work unless you know your divisors beforehand(at compile time, and even then requires divisor-specific codepaths) - resolving it run-time is not feasible, especially not in your case where performance matters.

    Also there's a previous question related to the subject which probably has interesting information on the matter from different points of view.

    0 讨论(0)
  • 2021-02-07 16:40

    Actually division by constants is a well known optimization for compilers and in fact, gcc is already doing it.

    This simple code snippet:

    int mod(int val) {
       return val % 10;
    }
    

    Generates the following code on my rather old gcc with -O3:

    _mod:
            push    ebp
            mov     edx, 1717986919
            mov     ebp, esp
            mov     ecx, DWORD PTR [ebp+8]
            pop     ebp
            mov     eax, ecx
            imul    edx
            mov     eax, ecx
            sar     eax, 31
            sar     edx, 2
            sub     edx, eax
            lea     eax, [edx+edx*4]
            mov     edx, ecx
            add     eax, eax
            sub     edx, eax
            mov     eax, edx
            ret
    

    If you disregard the function epilogue/prologue, basically two muls (indeed on x86 we're lucky and can use lea for one) and some shifts and adds/subs. I know that I already explained the theory behind this optimization somewhere, so I'll see if I can find that post before explaining it yet again.

    Now on modern CPUs that's certainly faster than accessing memory (even if you hit the cache), but whether it's faster for your obviously a bit more ancient CPU is a question that can only be answered with benchmarking (and also make sure your compiler is doing that optimization, otherwise you can always just "steal" the gcc version here ;) ). Especially considering that it depends on an efficient mulhs (ie higher bits of a multiply instruction) to be efficient. Note that this code is not size independent - to be exact the magic number changes (and maybe also parts of the add/shifts), but that can be adapted.

    0 讨论(0)
  • 2021-02-07 16:43

    Every power of 16 ends in 6. If you represent the number as a sum of powers of 16 (i.e. break it into nybbles), then each term contributes to the last digit in the same way, except the one's place.

    0x481A % 10 = ( 0x4 * 6 + 0x8 * 6 + 0x1 * 6 + 0xA ) % 10
    

    Note that 6 = 5 + 1, and the 5's will cancel out if there are an even number of them. So just sum the nybbles (except the last one) and add 5 if the result is odd.

    0x481A % 10 = ( 0x4 + 0x8 + 0x1 /* sum = 13 */
                    + 5 /* so add 5 */ + 0xA /* and the one's place */ ) % 10
                = 28 % 10
    

    This reduces the 16-bit, 4-nybble modulo to a number at most 0xF * 4 + 5 = 65. In binary, that is annoyingly still 3 nybbles so you would need to repeat the algorithm (although one of them doesn't really count).

    But the 286 should have reasonably efficient BCD addition that you can use to perform the sum and obtain the result in one pass. (That requires converting each nybble to BCD manually; I don't know enough about the platform to say how to optimize that or whether it's problematic.)

    0 讨论(0)
  • 2021-02-07 16:48

    If you want to do modulo 10 and shifts, maybe you can adapt double dabble algorithm to your needs?

    This algorithm is used to convert binary numbers to decimal without using modulo or division.

    0 讨论(0)
  • 2021-02-07 16:50

    Doing modulo 10 with bit shifts is going to be hard and ugly, since bit shifts are inherently binary (on any machine you're going to be running on today). If you think about it, bit shifts are simply multiply or divide by 2.

    But there's an obvious space-time trade you could make here: set up a table of values for out and out % 10 and look it up. Then the line becomes

      out += tab[out]
    

    and with any luck at all, that will turn out to be one 16-bit add and a store operation.

    0 讨论(0)
提交回复
热议问题