使用位移重新实现取模运算?

14

我正在为一个非常有限的系统编写代码,其中模运算非常缓慢。在我的代码中,每秒需要使用模约180次,我想尽可能地减少其使用次数,以显著提高代码的速度。目前我的主循环一次不像应该的那样在1/60秒内运行完。我想知道是否有可能仅使用位移操作重新实现模运算,就像使用乘法和除法一样。以下是我的c++代码(如果可以使用汇编来执行模运算,那就更好了)。如何在不使用除法或乘法的情况下删除模操作?

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

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

编辑:实际上我意识到我需要每秒超过180次以上的操作。因为输入值可能是一个长达40位数字的非常大的数。


2
每秒180次...在什么硬件上?对于现代非嵌入式处理器来说,这根本算不上什么。 - Mysticial
1
在一个16位处理器上。我知道这没什么,但还有很多其他代码需要在1/60秒内完成,而且模数运算需要在主循环的每个周期中进行三次。我想尽可能地提高速度。 - PgrAm
如果你能的话,不妨查看代码的汇编输出,这将提供更明确的有关发生了什么的信息。但对于项目来说并不是必需品。 - Shawn Buckley
1
@PgrAm: "我需要286支持". 什么?为什么?你住在哪个星球上? - ildjarn
2
40个数字?一个64位数只有19.1个数字。那你的数字怎么会有40个数字呢? - std''OrgnlDave
显示剩余7条评论
5个回答

24

使用简单的位运算,您可以通过将值(被除数)与除数-1进行AND运算来取模(除法)2的幂。以下是几个例子:

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(从10进制的4变为3),得到的位模式为00000011。在我们对原始值123和3进行按位与运算后,得到的结果位模式将是00000011。这在十进制中等于3。之所以需要一个2的幂次方除数,是因为一旦我们将其减1,就会得到所有较低有效位设置为1,其余位为0。一旦我们执行按位与运算,它会“抵消”原始值的更高有效位,并仅留下原始值除以除数的余数。
然而,对于任意除数应用特定的方法是行不通的,除非你事先知道你的除数(在编译时,甚至还需要特定于除数的代码路径)- 在运行时解决这个问题是不可行的,尤其是在你的情况下,性能很重要。
此外,还有一个相关主题的以前的问题,可能有不同角度的有趣信息。

1
我有一个类似的问题,为什么只有“(2的幂)-1”与模数一起使用。感谢您的解释! - whitehat

4

实际上,通过常量进行除法是编译器中已知的一种优化方法,事实上,gcc已经在使用该方法。

这段简单的代码:

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

在我比较老的gcc上使用-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

如果不考虑函数前/后语句,基本上有两个乘法(在x86上我们很幸运,可以使用lea替代其中一个),以及一些移位和加减运算。我知道我已经在某个地方解释了这种优化的理论,所以我会看看能否找到那篇文章,而不是再次解释。
现代CPU上,即使命中缓存,这肯定比访问内存要快,但对于您显然有点过时的CPU来说,它是否更快,只有通过基准测试才能回答(并确保您的编译器正在进行该优化,否则您始终可以在此“窃取”gcc版本 ;))。特别是考虑到它依赖于高效的mulhs(即乘法指令的高位),才能发挥效率。
请注意,此代码是大小无关的 - 要精确,魔术数字会改变(也可能是加/移位的部分),但可以进行适应。

2

使用位移进行模10运算将会很困难和丑陋,因为位移在任何现代计算机上都是二进制的。如果你仔细想一下,位移实际上就是乘以2或除以2。

但是这里有一个明显的时空权衡:设置一个outout%10值的表格并查找它。然后这行代码就变成了:

  out += tab[out]

如果运气好的话,这将变成一个16位加法和存储操作。


1
我只关心速度,不在乎难度或丑陋。然而,由于表的大小必须为40^10个元素,使用表将浪费太多内存。 - PgrAm
你需要再好好考虑一下。 - Charlie Martin
2
由于模运算在加法上具有分配律,因此您可以将其拆分为两个字节。然后,您需要一个仅包含512个条目的16位整数表。 - Raymond Chen
由于10可以被2整除,因此您只需要128个条目来处理最低有效位。之后,将其分成任意数量的较小部分仍然是有效的,但在某些时候,计算量将超过除法-乘法-减法算法。请注意,它是可分配的,但将总和转换回模数需要进行第二次模数操作,因此该算法变得递归。 - Potatoswatter

1

如果你想进行模10和移位操作,也许可以将双倍增算法调整为符合你的需求?

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


1

16的每个幂都以6结尾。如果您将数字表示为16的幂的总和(即将其分解为nybbles),则除了个位数之外,每个术语都以相同的方式对最后一位数字产生贡献。

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。在二进制中,这仍然很烦人,因此您需要重复算法(尽管其中一个不算)。

但是,286应该具有相当高效的BCD加法,您可以使用它来执行求和并一次性获得结果。(这需要手动将每个nybble转换为BCD;我对该平台的了解还不足以说明如何优化或是否存在问题。)


1
DAA - 十进制调整加法等内容应该很有用。 - sehe
嗯,286有22个时钟周期的16位除法。这样做要很难超越它,特别是没有移位器(!)。也许这仍然有帮助,具体取决于OP对“40位数字”的理解。同样,180次每秒会成为问题还不清楚。 - Potatoswatter

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接