【问题标题】:Need an efficient subtraction algorithm modulo a number需要一个有效的减法算法模数
【发布时间】:2012-12-11 08:35:39
【问题描述】:

对于给定的数字xyn,我想在C 中计算x-y mod n。看这个例子:

int substract_modulu(int x, int y, int n)
{
    return (x-y) % n;
}

只要x>y,我们都可以。然而,在另一种情况下,模运算是undefined

你可以想到x,y,n>0。我希望结果是正数,所以如果(x-y)<0,那么((x-y)-substract_modulu(x,y,n))/ n 应该是一个整数。

你知道的最快的算法是什么?有没有一种可以避免ifoperator? 的任何调用?

【问题讨论】:

  • 你可以使用abs(),但这不会更有效。
  • 您希望如何定义否定结果?如果y>x,理想的结果是什么?
  • 出于好奇,在什么情况下 IF 是昂贵的?如果它是密码学,那么自己实现它是一个非常危险的想法(除非它只是用于学习,而不是生产代码)。如果你有很多计算,也许你可以用 SSE 加快速度
  • IBM 的 PowerPC 处理器 (XBOX360) 和 PS3 PPU/SPU 真的不喜欢分支。它在控制台编程场景中非常有用。有时只做其他事情会更有效,即使它会导致更多指令。
  • 您使用 C++ 标记。是 C++ 还是 C?如果是 C++,看看我的回答。

标签: c++ c math modulus


【解决方案1】:

正如许多人所指出的,在当前的 C 和 C++ 标准中,x % n 不再是为 xn 的任何值定义的实现。在 x / n 未定义 [1] 的情况下,这是未定义的行为。此外,x - y 在整数溢出的情况下是未定义的行为,如果xy 的符号可能不同,则可能发生这种情况。

因此,通用解决方案的主要问题是避免整数溢出,无论是在除法还是减法中。如果我们知道xy 是非负的并且n 是正的,那么溢出和被零除是不可能的,我们可以自信地说(x - y) % n 是定义的。不幸的是,x - y 可能是负数,在这种情况下,% 运算符的结果也是如此。

如果我们知道n 是正数,就很容易纠正负数的结果;我们所要做的就是无条件地添加n 并执行另一个modulo 操作。这不太可能是最好的解决方案,除非你有一台除法比分支快的计算机。

如果有条件加载指令可用(现在很常见),那么编译器可能会很好地处理以下代码,这些代码可移植且定义明确,受x,y ≥ 0 ∧ n > 0 的约束:

((x - y) % n) + ((x >= y) ? 0 : n)

例如,gcc 为我的核心 I5 生成此代码(尽管它的通用性足以在任何非古生代英特尔芯片上工作):

    idivq   %rcx
    cmpq    %rsi, %rdi
    movl    $0, %eax
    cmovge  %rax, %rcx
    leaq    (%rdx,%rcx), %rax

这是令人愉快的无分支。 (条件移动通常比分支快很多。)

另一种方法是(除了需要编写函数sign):

((x - y) % n) + (sign(x - y) & (unsigned long)n)

如果 sign 的参数为负数,则其全为 1,否则为 0。符号的一种可能实现(改编自 bithacks)是

unsigned long sign(unsigned long x) {
  return x >> (sizeof(long) * CHAR_BIT - 1);
}

这是可移植的(定义了将负整数值转换为无符号),但在缺乏高速移位的架构上可能会很慢。它不太可能比以前的解决方案更快,但是 YMMV。 TIAS。

对于可能整数溢出的一般情况,这些都不会产生正确的结果。处理整数溢出非常困难。 (一个特别烦人的情况是n == -1,尽管您可以对其进行测试并返回 0 而无需使用任何%。)此外,您需要确定您对负n 的模结果的偏好。我个人更喜欢 x%n 为 0 或与 n 具有相同符号的定义——否则你为什么要使用负除数——但应用程序不同。

如果n 不是-1 并且n + n 不溢出,Tom Tanner 提出的三模解决方案将有效。如果xyINT_MINn == -1 将失败,如果nINT_MIN,则使用abs(n) 而不是n 的简单修复将失败。 n 具有较大绝对值的情况可以用比较来代替,但是有很多极端情况,并且由于标准不需要 2 的补码运算,因此变得更加复杂,因此不容易预测什么是极端情况是 [2]。

最后一点,一些诱人的解决方案不起作用。你不能只取(x - y)的绝对值:

(-z) % n == -(z % n) == n - (z % n) ≠ z % n(除非z % n 恰好是n / 2

而且,出于同样的原因,您不能只取模结果的绝对值。

此外,您不能只将(x - y) 转换为无符号:

(unsigned)z == z + 2<sup>k</sup> (for some k) if z &lt; 0
(z + 2<sup>k</sup>) % n == (z % n) + (2<sup>k</sup> % n) ≠ z % n 除非(2<sup>k</sup> % n) == 0


[1] 如果n==0x/nx%n 都是未定义的。但是如果x/n 是“不可表示的”(即存在整数溢出),x%n 也未定义,这将发生在二进制补码上 如果x 是最负的可表示数字且n == -1,则机器(即您关心的所有机器)。很清楚为什么在这种情况下 x/n 应该是未定义的,但在 x%n 的情况下应该是未定义的,因为该值(数学上)是 0

[2] 大多数抱怨预测浮点运算结果困难的人并没有花太多时间尝试编写真正可移植的整数运算代码:)

【讨论】:

  • 谢谢。您的帖子在 TomTanners 的帖子中发现了一些问题。不过有一件事:如果 x-y
  • @Johannes:我将 n 添加到 (x-y)%n,而不是 x-y(x-y)%n &gt; (-n)(x-y) 不溢出的所有情况下。
  • @Johannes,顺便说一下,您可以分两步进行测试和添加解决方案,例如:int tmp = (x - y) % n; return tmp + (tmp &lt; 0) ? n : 0;,即使使用 C++11 之前的保证也可以。事实上,我相信 C++11 和 C++03 都使用相同的除法操作码来做模运算; C++11 中改进的保证只是将所有现代硬件都以这种方式工作的现实编码。 C99 也有同样的保证,那是十多年前的事了。
  • @Johannes,顺便说一句,我之前忘了提这个。如果已知n,则可以用乘法代替除法。众所周知,我的意思是它要么是一个编译时常量,要么你会经常使用它,以至于值得做一些计算来确定将什么用作乘数。我相信你可以在互联网上的某个地方找到详细信息。如果n 在编译时已知,gcc 和 clang 都可以为你解决,所以你不必知道细节。
  • 一些澄清:我不是建议将 x - y 转换为无符号,而是将 x 和 y 都转换为无符号。另外,如果 n 是 -ve,我认为这个函数没有任何意义。
【解决方案2】:

如果你想避免未定义的行为,没有 if,以下将起作用

return (x % n - y % n + n) % n;

效率取决于模运算的实现,但我怀疑涉及if 的算法会更快。

或者,您可以将 xy 视为未签名。在这种情况下,不涉及负数,也没有未定义的行为。

【讨论】:

  • 谢谢,第一个看起来不错。但是你对 unsigned 方法是什么意思?
  • 我认为你可以放弃对 x 的模运算。
  • @Johannes - 如果 x 和 y 是无符号整数,那么 x - y 永远不会是负数。 n 在任何情况下都应该是无符号的,模负数没有多大意义
  • @TomTanner 好的,假设x-y=-1,然后是无符号的(假设我们有char 而不是int,为方便起见),x-y=255。但是如果n=3,那么-1%3=2!=0=255%3。你确定这行得通吗?
  • 进行两次模运算几乎可以肯定是不好的。任意数的模(不是二的幂)是通过除法完成的。除法是我所知道的每个处理器中相当慢的操作。而且分频单元的数量通常很少,所以不能并行进行。例如,AMD 的优化指南为 IDIV 提供了 71 个时钟周期的延迟时间(64 位数字,32 位数字是 39 个时钟周期)。我相信英特尔处理器是相似的,而且几乎所有处理器都比这差。
【解决方案3】:

在 C++11 中,未定义的行为已被删除。根据您想要的确切行为,您可以坚持使用

return (x-y) % n;

完整的解释请阅读这个答案:

https://stackoverflow.com/a/13100805/1149664

对于 n==0 或者如果 x-y 无法存储在您使用的类型中,您仍然会得到未定义的行为。

【讨论】:

  • 然而,就我而言,我不能使用 C++11。但真的很有趣!
【解决方案4】:

分支是否重要在某种程度上取决于 CPU。根据文档absMSDN)具有内在行为,它可能根本不是瓶颈。您必须对此进行测试。

如果您不想无条件地计算事物,可以从 Bit Twiddling Hacks site 改编几种不错的方法。

int v;           // we want to find the absolute value of v
unsigned int r;  // the result goes here 
int const mask = v >> sizeof(int) * CHAR_BIT - 1;

r = (v + mask) ^ mask;

但是,如果没有有关硬件目标和测试的更多信息,我不知道这是否会对您的情况有所帮助。

出于好奇,我不得不自己对此进行测试,当您查看编译器生成的程序集时,我们可以看到使用 abs 并没有真正的开销。

unsigned r = abs(i);
====
00381006  cdq              
00381007  xor         eax,edx 
00381009  sub         eax,edx

以下只是上述示例的另一种形式,根据 Bit Twiddling Site 的说法,它没有获得专利(而 Visual C++ 2008 编译器使用的版本是)。

在我的回答中,我一直在使用 MSDN 和 Visual C++,但我认为任何理智的编译器都有类似的行为。

【讨论】:

    【解决方案5】:

    假设0 &lt;= x &lt; n0 &lt;= y &lt; n,那么(x + n - y) % n 呢?那么 x + n 肯定会大于 y,减去 y 总是会得到一个正整数,最后的 mod n 必要时会减少结果。

    【讨论】:

    • 好点。就我而言,确实如此。否则,我可以计算(x+n-(y%n))%n。这确实与 TomTanner 的帖子相同。
    【解决方案6】:

    我猜这里的情况并非如此,但我想提一下,如果你取模的值是 2 的幂,那么使用“AND”方法会快很多(我将忽略 xy,只展示它对单个 x 的工作原理,因为 xy 不是这里等式的一部分):

    int modpow2(int x, int n)
    {
        return x & (n-1);
    }
    

    如果您想确保您的代码不会做任何愚蠢的事情,您可以添加 ASSERT(!(n &amp; n-1)); - 这会检查 n 中是否只有一个位设置(因此,n 是 2 的幂)。

    【讨论】:

      【解决方案7】:

      这是我在竞争性编程中使用的 CPP 代码:

      #include <iostream>
      #include<bits/stdc++.h>
      using namespace std;
      #define ll          long long
      #define  mod         1000000007
      
      ll subtraction_modulo(ll x, ll y ){
              return ( ( (x - y) %  mod ) +  mod ) %  mod;
          }
      

      这里,

      ll -> long long int

      mod -> 要使用的全局定义的 mod 值。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 2020-04-17
        • 1970-01-01
        • 1970-01-01
        • 2021-03-26
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多