34

我曾经在某处读到模数运算符在小型嵌入式设备上效率低下,例如没有整数除法指令的 8 位微控制器。也许有人可以证实这一点,但我认为差异比整数除法运算慢 5-10 倍。

除了保持计数器变量并在 mod 点手动溢出到 0 之外,还有其他方法吗?

const int FIZZ = 6;
for(int x = 0; x < MAXCOUNT; x++)
{
    if(!(x % FIZZ)) print("Fizz\n"); // slow on some systems
}

与:

我目前这样做的方式:

const int FIZZ = 6;
int fizzcount = 1;
for(int x = 1; x < MAXCOUNT; x++)
{
    if(fizzcount >= FIZZ) 
    {
        print("Fizz\n");
        fizzcount = 0;
    }
}
4

13 回答 13

47

啊,按位算术的乐趣。许多除法例程的副作用是模数 - 因此在少数情况下除法实际上应该比模数更快。我有兴趣查看您从中获得此信息的来源。具有乘法器的处理器使用乘法器具有有趣的除法例程,但是您只需另外两个步骤(乘法和减法)就可以从除法结果到模数,因此它仍然具有可比性。如果处理器具有内置的除法例程,您可能会看到它还提供了余数。

尽管如此,如果您真的想了解如何优化模运算,则需要研究模数运算的一小部分数论分支。例如,模块化算术对于生成幻方非常方便。

因此,在这种情况下,以下是对 x 示例的模数数学的一个非常低级的研究,它应该向您展示它与除法相比有多么简单:


也许考虑这个问题的更好方法是根据数基和模算术。例如,您的目标是计算 DOW mod 7,其中 DOW 是星期几的 16 位表示。你可以这样写:

 DOW = DOW_HI*256 + DOW_LO

 DOW%7 = (DOW_HI*256 + DOW_LO) % 7
       = ((DOW_HI*256)%7  + (DOW_LO % 7)) %7
       = ((DOW_HI%7 * 256%7)  + (DOW_LO%7)) %7
       = ((DOW_HI%7 * 4)  + (DOW_LO%7)) %7

以这种方式表示,您可以分别计算高字节和低字节的模 7 结果。将高位的结果乘以 4 并将其与低位相加,然后最终以 7 为模计算结果。

计算 8 位数字的 mod 7 结果可以以类似的方式执行。你可以像这样用八进制写一个 8 位数字:

  X = a*64 + b*8 + c

其中 a、b 和 c 是 3 位数字。

  X%7 = ((a%7)*(64%7) + (b%7)*(8%7) + c%7) % 7
      = (a%7 + b%7 + c%7) % 7
      = (a + b + c) % 7

自从64%7 = 8%7 = 1

当然,a、b、c 是

  c = X & 7
  b = (X>>3) & 7
  a = (X>>6) & 7  // (actually, a is only 2-bits).

a+b+c的最大可能值为7+7+3 = 17。因此,您将需要一个八进制步骤。完整的(未经测试的)C 版本可以这样写:

unsigned char Mod7Byte(unsigned char X)
{
    X = (X&7) + ((X>>3)&7) + (X>>6);
    X = (X&7) + (X>>3);

    return X==7 ? 0 : X;
}

我花了一些时间写了一个 PIC 版本。实际实现与上面描述的略有不同

Mod7Byte:
       movwf        temp1        ;
       andlw        7        ;W=c
       movwf        temp2        ;temp2=c
       rlncf   temp1,F        ;
       swapf        temp1,W ;W= a*8+b
       andlw   0x1F
       addwf        temp2,W ;W= a*8+b+c
       movwf        temp2   ;temp2 is now a 6-bit number
       andlw   0x38    ;get the high 3 bits == a'
       xorwf        temp2,F ;temp2 now has the 3 low bits == b'
       rlncf   WREG,F  ;shift the high bits right 4
       swapf   WREG,F  ;
       addwf        temp2,W ;W = a' + b'

 ; at this point, W is between 0 and 10


       addlw        -7
       bc      Mod7Byte_L2
Mod7Byte_L1:
       addlw        7
Mod7Byte_L2:
       return

这是一个测试算法的小程序

       clrf    x
       clrf    count

TestLoop:
       movf        x,W
       RCALL   Mod7Byte
       cpfseq count
        bra    fail

       incf        count,W
       xorlw   7
       skpz
        xorlw        7
       movwf   count

       incfsz        x,F
       bra        TestLoop
passed:

最后,对于 16 位结果(我没有测试过),你可以这样写:

uint16 Mod7Word(uint16 X)
{
 return Mod7Byte(Mod7Byte(X & 0xff) + Mod7Byte(X>>8)*4);
}

斯科特


于 2008-09-07T03:25:09.353 回答
36

如果您正在计算一个数字模数为 2 的某个幂,则可以使用按位和运算符。只需从第二个数字中减去一个。例如:

x % 8 == x & 7
x % 256 == x & 255

一些警告:

  1. 仅适用于第二个数字是 2 的幂。
  2. 仅当模数始终为正时才等效。当第一个数字为负数时,C 和 C++ 标准没有指定模数的符号(直到 C++11,它确实保证它是负数,这是大多数编译器已经在做的)。逐位并去掉符号位,因此它将始终为正数(即,它是真正的模数,而不是余数)。听起来这就是你想要的。
  3. 您的编译器可能已经在可能的情况下执行此操作,因此在大多数情况下,不值得手动执行此操作。
于 2008-09-07T02:57:32.480 回答
6

在大多数情况下,使用不是 2 的幂的模数会产生开销。这与处理器无关,因为(AFAIK)即使具有模数运算符的处理器比掩码操作慢几个周期。

在大多数情况下,这不是一个值得考虑的优化,当然也不值得计算您自己的快捷操作(尤其是当它仍然涉及除法或乘法时)。

然而,一个经验法则是选择数组大小等为 2 的幂。

因此,如果计算星期几,那么无论是否设置大约 100 个条目的循环缓冲区,都可以使用 %7 ......为什么不将其设为 128。然后您可以编写 % 128 并且大多数(所有)编译器都会使这个 & 0x7F

于 2008-09-13T21:07:12.870 回答
4

除非您真的需要在多个嵌入式平台上获得高性能,否则在您进行分析之前,不要出于性能原因更改您的编码方式!

为优化性能而编写的笨拙的代码很难调试和维护。编写一个测试用例,并在您的目标上对其进行概要分析。一旦您知道模数的实际成本,然后决定替代解决方案是否值得编码。

于 2008-09-07T02:48:27.227 回答
3

@Matthew 是对的。试试这个:

int main() {
  int i;
  for(i = 0; i<=1024; i++) {
    if (!(i & 0xFF)) printf("& i = %d\n", i);
    if (!(i % 0x100)) printf("mod i = %d\n", i);
  }
}
于 2008-09-07T03:16:24.197 回答
3
x%y == (x-(x/y)*y)

希望这可以帮助。

于 2015-04-15T16:37:48.353 回答
1

在嵌入式世界中,您需要执行的“模数”操作通常可以很好地分解为您可以使用的位操作,&有时|>>.

于 2008-09-07T02:35:08.647 回答
1

您可以访问嵌入式设备上的任何可编程硬件吗?比如柜台之类的?如果是这样,您也许可以编写基于硬件的模块单元,而不是使用模拟的 %。(我在 VHDL 中做过一次。但不确定我是否还有代码。)

请注意,您确实说过除法速度要快 5-10 倍。您是否考虑过进行除法、乘法和减法来模拟 mod?(编辑:误解了原帖。我确实认为除法比mod快很奇怪,它们是相同的操作。)

但是,在您的特定情况下,您正在检查 6. 6 = 2*3 的 mod。因此,如果您首先检查最低有效位是否为 0,您可能会获得一些小的收益。例如:

if((!(x & 1)) && (x % 3))
{
    print("Fizz\n");
}

但是,如果您这样做,我建议您确认您获得了任何收益,对分析器来说是这样。并做一些评论。我会为下一个必须查看代码的人感到难过。

于 2008-09-07T02:56:21.500 回答
1

你真的应该检查你需要的嵌入式设备。我见过的所有汇编语言(x86、68000)都使用除法来实现模数。

实际上,除法汇编操作将除法的结果和余数返回到两个不同的寄存器中。

于 2008-09-07T03:19:01.133 回答
0

并不是说这一定会更好,但是您可以有一个始终上升到 的内部循环FIZZ,以及一个将所有循环重复一定次数的外部循环。如果MAXCOUNT不能被FIZZ.

也就是说,我建议在您的预期平台上进行一些研究和性能分析,以清楚地了解您所面临的性能限制。可能有更高效的地方可以花费您的优化工作。

于 2008-09-07T02:52:27.157 回答
0

@Jeff V:我发现它有问题!(除了您的原始代码正在寻找一个 mod 6 之外,现在您实际上是在寻找一个 mod 8)。你继续做额外的+1!希望您的编译器可以优化它,但为什么不从 2 开始测试并转到 MAXCOUNT 包括在内呢?最后,每次 (x+1) 不能被 8 整除时,您都会返回 true。这是您想要的吗?(我认为是,但只是想确认一下。)

于 2008-09-07T03:16:31.187 回答
0

对于模 6,您可以将 Python 代码更改为 C/C++:

def mod6(number):
    while number > 7:
        number = (number >> 3 << 1) + (number & 0x7)
    if number > 5:
        number -= 6
    return number
于 2019-03-03T14:33:37.150 回答
-4

打印语句所需的时间甚至比模数运算符的最慢实现要长几个数量级。所以基本上评论“在某些系统上很慢”应该是“在所有系统上都很慢”。

此外,提供的两个代码片段不会做同样的事情。在第二个中,行

如果(fizzcount >= FIZZ)

始终为假,因此永远不会打印“FIZZ\n”。

于 2008-09-12T16:36:59.957 回答