【问题标题】:How does C perform the % operation interallyC如何在内部执行%操作
【发布时间】:2013-06-24 01:01:59
【问题描述】:

我很想了解 mod 操作背后的逻辑,因为我知道可以执行位移操作来做不同的事情,例如位移乘法。

我可以看到它正在完成的一种方法是通过递归算法不断划分,直到你不能再划分,但这似乎没有效率。

任何想法都会有所帮助。提前致谢!

【问题讨论】:

  • 通常,*div 指令在两个寄存器中为您提供商和余数。只取剩余部分。
  • 取决于平台。在大多数现代处理器上,有一条 div 指令(或等效指令)可以直接在硬件中执行此操作。
  • 你可以用 c 编写一个简单的程序来修改,然后运行 ​​gcc -S simple_program.c 来查看汇编输出
  • 点击链接查看remainder is a result of the div instruction的实际演示。
  • 正如其他人所说,它通常在硬件指令中完成。但对于算法的观点,请参见:en.wikipedia.org/wiki/…

标签: c modulo


【解决方案1】:

虽然大多数 C 实现在具有除法指令的硬件上运行,但余数运算可以大致如下执行,用于计算 p%q,假设无符号值:

#define HI (-1U-(-1U/2))
unsigned i;
for (i=0; !(HI & (q<<i)); i++);
do { if (p >= (q<<i)) p -= (q<<i); } while (i--);

得到的余数在p中。

【讨论】:

    【解决方案2】:

    除了使用移位的硬件指令和实现,如R.. suggests,还有reciprocal multiplication

    % 的右侧是一个常量时,可以使用这种技术,在编译时就知道了。
    倒数乘法用于实现除法,但对% 使用它很容易,基于公式a%b == a-(a/b)*b

    【讨论】:

    • 我想你的意思是a%b == a-(a/b)*b
    【解决方案3】:

    快速版本是:取决于硬件、优化器、if it's division by a constant or not (pdf)、是否需要检查异常(例如,以 0 为模)、是否以及如何处理负数(this is a scary question for C++)等...

    R 为无符号整数提供了一个简洁明了的答案,但除非您精通 C,否则很难理解。

    R 所阐明的技术的关键是去除q 的倍数,直到不再有q 的倍数。我们可以用一个简单的循环天真地做到这一点:

    while (p >= q) p -= q; // One liner, woohoo!
    

    代码可能很短,但是对于较大的 p 值和较小的 q 值,这可能需要很长时间。

    比一次剥离一个q 更好的是一次剥离多个q。请注意,我们实际上希望尽可能多地去除q —— 即,floor(p/q) 许多q ......事实上,这是一种有效的技术。对于无符号整数,人们会期望p % q == p - (p / q) * q。 (请注意,无符号整数除法会向下舍入。)

    但这几乎感觉像是在作弊,因为除法和余数运算是如此密切相关。 (事实上​​,如果硬件本身支持除法,它通常支持除法计算余数运算,因为它们之间的关系非常密切。)

    假设我们无法使用除法,我们如何找到大于 1 的 q 的倍数来去除?在硬件中,固定移位操作很便宜(如果实际上不是免费的),并且在概念上表示乘以 2 的非负幂。例如,将位串左移 3 相当于乘以 8(即 2^3),例如十进制的 5 相当于二进制的 '101'。通过在右边添加三个零(给出“101000”)将二进制“101”移位,结果是十进制的 50——五乘以八。

    同样,班次操作与软件操作一样非常便宜,而且您很难快速找到不支持它们的控制器。 (一些架构,如 ARM 甚至可以将移位与其他指令结合起来,使它们在很多时候“空闲”。)

    武装(无法抗拒)这些移位操作,我们可以进行如下操作:

    1. 找出我们可以将q 乘以但仍小于p 的2 的最大幂。
    2. 从 2 的最大幂到最小幂,将 q 乘以 2 的每个幂,如果小于 p 的余数,则从 p 的余数中减去。
    3. 剩下的就是剩下的。

    为什么会这样?因为最后你会发现所有的 2 的减幂实际上总和为floor(p / q)!不要相信我的话,类似的知识以very long time而闻名。

    分解 R 的答案:

    #define HI (-1U-(-1U/2))
    

    这有效地为您提供了一个仅设置了最高值位的无符号整数。

    unsigned i;
    for (i=0; !(HI & (q<<i)); i++);
    

    这一行实际上发现两个q 的最高幂可以在溢出一个无符号整数之前相乘。这不是绝对必要的,但除了增加所需的执行时间外,它不会改变结果。

    如果您不熟悉这一行中的 C-isms:

    1. (q&lt;&lt;i)i 的左位移位。回想一下,这相当于乘以 2^i
    2. HI &amp; (q&lt;&lt;i) 执行按位与。由于HI 仅填充了其最高位,因此只有当(q&lt;&lt;i) 大到足以导致最高位为非零时,才会导致非零值。再向左移动一次,就会出现整数溢出。
    3. (HI &amp; (q&lt;&lt;i)) 为零时,!(HI &amp; (q&lt;&lt;i)) 为“真”,否则为“假”。

    do { if (p &gt;= (q&lt;&lt;i)) p -= (q&lt;&lt;i); } while (i--);

    这是一个简单的递减循环do { .... } while (i--);。请注意,在i 上使用了后减法,因此循环执行,然后它检查i 是否不为零,然后从i 中减去1,然后如果其先前的检查导致true 它继续。当i 为0 时,它具有循环最后一次执行的属性。这很重要,因为我们可能需要剥离q 的未乘副本。

    if (p &gt;= (q&lt;&lt;i)) 检查 2^i * q 是否小于或等于 p。如果是,p -= (q&lt;&lt;i) 将其剥离。

    剩下的。

    【讨论】:

    • +1 如果我想花那么多时间在上面,这正是我会写来补充我的答案的。 :-)
    【解决方案4】:

    根据优化器的智能,有一个取模基数为 2 的快捷方式。例如,a % 32 可以实现为 a &amp; 31。一般来说,a % (2^N) == a &amp; (2^N -1)。与除法相比,这快如闪电。大多数除法器(无论是硬件)都需要至少 1 个周期来计算结果的每一位,而逻辑与只是几个周期的运算(在流水线中)。

    编辑:这仅在 a 未签名时有效!

    【讨论】:

    • 如果您正在执行a % 256a % 65536,则使用演员阵容会更快:a % 256 == (uint8_t) aa % 65536 == (uint16_t) a。 (使用#include &lt;stdint.h&gt;
    • 不幸的是,从 C11 开始,如果 aint,则 a % 256 不能优化为 (uint8_t)aa &amp; 255。相反,编译器必须生成更像a &lt; 0 ? a | -256 : a &amp; 255 的东西。我宁愿有一个标准来指定如果x 和/或y 是负数并且x 不是y 的倍数,x % y 可以产生任意的非零值。强制行为几乎没有用处,也没有在旧标准中指定,那么为什么需要编译器为其生成额外的代码呢?
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2020-06-02
    • 2015-04-09
    • 2015-04-16
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多