除了在 C/C++ 中使用 %(模数)之外,还有其他选择吗?

Is there any alternative to using % (modulus) in C/C++?

提问人:JeffV 提问时间:9/7/2008 最后编辑:Kirill KobelevJeffV 更新时间:6/9/2020 访问量:60326

问:

我曾经在某处读到,模运算符在小型嵌入式设备(如 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;
    }
}
C++ C 模嵌入

评论

1赞 An̲̳̳drew 11/26/2008
在第二个示例中,您忘记增加 fizzcount。
1赞 Dan 6/26/2013
I read somewhere once that the modulus operator is inefficient...我相信你可能会想到奈杰尔·琼斯(Nigel Jones)关于这个主题的一篇博客文章。在这篇文章中,他比较了不同 uC 的模量计算......ARM Cortex 为 390 个周期,MSP430 和 AVR 为 30,000 个周期。
0赞 JeffV 7/4/2013
谢谢 Dan,我很高兴我现在使用的是 ARM 皮层。

答:

1赞 Paul Tomblin 9/7/2008 #1

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

评论

0赞 JeffV 9/7/2008
如果它不是整齐的二次幂,你会如何处理这种情况?
2赞 Paul Tomblin 9/11/2008
Jeff V:关于数学的有趣事实:x * 7 == x + (x * 2) + (x * 4),或 x + x >> 1 + x >> 2。整数加法通常非常便宜。
4赞 shmuelp 9/7/2008 #2

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

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

评论

0赞 JeffV 9/7/2008
我 100% 同意你的看法,但是,在性能很重要的情况下,您是否有模数的替代方案?
0赞 shmuelp 9/9/2008
我没有具体的选择。如果我不能像 @Paul-Tomblin 建议的那样使用位掩码或右移,我会保留一个计数器,就像您在问题中建议的那样。当然,我习惯于使用更多的开销(我们在处理器上运行 CORBA)。
0赞 Matt Sheppard 9/7/2008 #3

并不是说这一定更好,但你可以有一个总是上升到 的内循环,以及一个重复一定次数的外循环。然后,如果不能被 整除,则最后几步可能必须得到特殊情况。FIZZMAXCOUNTFIZZ

也就是说,我建议对你的目标平台进行一些研究和性能分析,以清楚地了解你所受到的性能限制。可能有更高效的地方可以花费您的优化工作。

1赞 Rob Rolnick 9/7/2008 #4

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

请注意,您确实说过除法速度快 5-10 倍。你有没有考虑过做一个除法、乘法和减法来模拟这个模组?(编辑:误解了原来的帖子。我确实觉得除法比mod快很奇怪,它们是相同的操作。

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

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

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

评论

0赞 JeffV 9/7/2008
我的意思是,在指令集中具有整数除法操作码的微观设备上,除法和模数要快 5-10 倍。
0赞 Rob Rolnick 9/7/2008
哦,对不起,我误会了。(显然。你有没有介绍过其他建议?
36赞 Matthew Crumley 9/7/2008 #5

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

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

需要注意的几点:

  1. 仅当第二个数字是 2 的幂时,这才有效
  2. 只有当模量始终为正时,它才是等价的。当第一个数字为负数时,C 和 C++ 标准没有指定模数的符号(直到 C++11,这确实保证它是负数,这是大多数编译器已经在做的事情)。逐位并去掉符号位,因此它始终为正数(即它是真正的模数,而不是余数)。不过,这听起来像是你想要的。
  3. 你的编译器可能已经在可以的时候这样做了,所以在大多数情况下,不值得手动这样做。

评论

5赞 JeffV 6/15/2012
2 的幂是简单的情况。
0赞 Johan Lundberg 6/26/2013
“当第一个数字为负时,C 和 C++ 标准没有指定模数的符号。此问题现已在 C++11 中得到解决。stackoverflow.com/questions/12710801/c-operator-guarantees
3赞 hoyhoy 9/7/2008 #6

@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);
  }
}
0赞 Rob Rolnick 9/7/2008 #7

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

评论

0赞 JeffV 9/7/2008
是的,我也看到了。我把它改成 7,因为 6 最终可以被 2 整除。我正在寻找一个可用于预先已知的任何数字的答案。
0赞 greybeard 1/1/2023
I'm looking for an answer that could be used for any number known [at compile time].编辑有助于在问题正文中有效回答问题的信息!(您可能知道 14 年&50k 代表后......
1赞 Vincent Robert 9/7/2008 #8

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

实际上,除法装配操作在两个不同的寄存器中返回除法的结果和剩余结果。

48赞 Adam Davis 9/7/2008 #9

啊,按位算术的乐趣。许多除法例程的一个副作用是模数 - 因此在少数情况下,除法实际上应该比模数快。我很想看看你从哪里得到这些信息。带有乘法器的处理器使用乘法器具有有趣的除法例程,但您只需再走两步(乘法和减法)即可从除法结果获得模数,因此它仍然具有可比性。如果处理器具有内置的除法例程,您可能会看到它也提供了余数。

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

因此,本着这种精神,这里以 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).

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

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

下面是测试算法的 liitle 例程

       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);
}

斯科特


评论

5赞 JeffV 9/7/2008
哇,如果我必须为 7 以外的数字做这件事,我将不得不回来重新阅读这篇文章。
-4赞 Greg Rogers 9/13/2008 #10

print 语句所需的时间甚至比模运算符的最慢实现要长几个数量级。因此,基本上“在某些系统上速度慢”的评论应该是“在所有系统上速度慢”。

此外,提供的两个代码片段不会执行相同的操作。在第二条中,线

if(fizzcount >= FIZZ)

始终为 false,因此从不打印“FIZZ\n”。

6赞 itj 9/14/2008 #11

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

在大多数情况下,这不是一个值得考虑的优化,当然也不值得计算你自己的快捷方式运算(特别是如果它仍然涉及除法或乘法)。

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

因此,如果计算星期几,不妨使用 %7 如果设置一个大约 100 个条目的循环缓冲区......为什么不让它成为 128。然后你可以写 % 128,大多数(所有)编译器都会这样做 & 0x7F

3赞 Zeeshan 4/16/2015 #12
x%y == (x-(x/y)*y)

希望这会有所帮助。

评论

0赞 étale-cohomology 9/4/2016
这正是我一直在寻找的!
0赞 MSalters 6/9/2020
在缺少硬件除法指令的微控制器上,模数很慢。所以这实际上并不能解决问题。
0赞 TigerTV.ru 3/3/2019 #13

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

def mod6(number):
    while number > 7:
        number = (number >> 3 << 1) + (number & 0x7)
    if number > 5:
        number -= 6
    return number

评论

0赞 greybeard 1/1/2023
(什么Python 代码?提到使用基数 4 计算 mod 3 类似于基数 10 中的 mod 9。将 LSB 排除在环路之外。