使用位移重新实现模数?

时间:2012-06-18 02:00:00

标签: c++ optimization bit-manipulation modulo bit-shift

我正在为一个非常有限的系统编写一些代码,其中mod运算符非常慢。在我的代码中,模数需要每秒使用大约180次,并且我认为尽可能地删除它会显着提高代码的速度,因为现在我的主循环的一个循环不会在1/60的情况下运行应该是第二个。我想知道是否有可能仅使用乘法和除法可能的位移来重新实现模数。所以这是我目前在c ++中的代码(如果我可以使用汇编执行模数,那就更好了)。如何在不使用除法或乘法的情况下删除模数?

    while(input > 0)
{
    out = (out << 3) + (out << 1);
    out += input % 10;

    input = (input >> 8) + (input >> 1);
}

编辑:实际上我意识到我需要每秒执行180次以上。看作输入的值可以是一个非常大的数字,最多40位数。

5 个答案:

答案 0 :(得分:15)

使用简单按位运算可以做的是通过将它与除数-1进行AND运算,取值(除数)的二次幂(除数)。几个例子:

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). 

为什么会这样?让我们考虑值123的位模式,即1111011,然后是除数4,其位模式为00000100。正如我们现在知道的那样,除数必须是2的幂(如4),我们需要将它递减1(从4到3的十进制),这就产生了位模式00000011。在我们对原始的123和3进行按位与AND之后,得到的位模式将为00000011。结果是十进制的3。我们需要二次幂除数的原因是,一旦我们将它们递减一,我们将所有不太重要的位设置为1,其余为0。一旦我们按位进行AND运算,它就会“取消”原始值中更重要的位,并简单地将原始值的剩余部分除以除数。

然而,除非你事先知道你的除数(在编译时,甚至那时需要除数特定的代码路径),否则对任意除数应用这样的特定事物是行不通的 - 解析它的运行时是不可行的,尤其不是在你的表现很重要的情况下。

此外还有a previous question related to the subject从不同的角度来看可能有关于这个问题的有趣信息。

答案 1 :(得分:2)

使用位移进行模10会变得困难和丑陋,因为位移本质上是二进制的(在今天你将要运行的任何机器上)。如果你考虑一下,位移只是乘以或除以2。

但是您可以在此处进行明显的时空交易:为outout % 10设置一个值表并进行查找。然后该行成为

  out += tab[out]

如果运气好的话,那将是一个16位的添加和商店操作。

答案 2 :(得分:2)

实际上,常量除法是众所周知的编译器优化,事实上,gcc已经在做了。

这个简单的代码段:

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

使用-O3:

在我相当老的gcc上生成以下代码
_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

如果你忽略了函数epilogue / prologue,基本上是两个muls(确实在x86上我们很幸运,可以使用lea一个)和一些移位和添加/ subs。我知道我已经在某处解释了这种优化背后的理论,所以我会在再次解释之前看看是否可以找到该帖子。

现在在现代CPU上,这肯定比访问内存更快(即使你点击了缓存),但是它是否更快,显然更古老的CPU是一个问题,只能通过基准测试来回答(并确保你的编译器正在进行优化,否则你总是可以在这里“窃取”gcc版本;))。特别是考虑到它依赖于有效的mulhs(即乘法指令的较高位)是有效的。 请注意,此代码大小独立 - 确切地说是幻数变化(也可能是添加/移位的一部分),但可以进行调整。

答案 3 :(得分:1)

如果你想做模10和转换,也许你可以根据自己的需要调整 double dabble algorithm

此算法用于将二进制数转换为十进制数而不使用模数或除法。

答案 4 :(得分:1)

每个16的幂都以6结尾。如果你将数字表示为16的幂之和(即将其分解为nybbles),那么每个术语以相同的方式对最后一个数字做出贡献,除了一个位置。 / p>

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

注意6 = 5 + 1,如果有偶数,5将取消。所以只需对nybbles(除了最后一个)求和,如果结果是奇数,则加5.

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

这会将16位,4-nybble模数减少到最多0xF * 4 + 5 = 65的数字。在二进制文件中,令人烦恼的仍然是3个nybbles,所以你需要重复算法(尽管其中一个算法并不算数)。

但是286应该具有合理有效的BCD添加,您可以使用它来执行总和并在一次通过中获得结果。 (这需要手动将每个nybble转换为BCD;我不太了解该平台如何优化它或是否存在问题。)