有没有办法写" mod 31"没有模数/除法运算符?

时间:2014-09-25 20:08:32

标签: c bit-manipulation bitwise-operators modulus

如果操作数是2的幂,则可以在没有模数运算符或除法的情况下轻松获得数字的模数。在这种情况下,以下公式成立:x % y = (x & (y − 1))。在许多架构中,这通常很有效。可以对mod 31进行同样的操作吗?

int mod31(int a){ return a % 31; };

5 个答案:

答案 0 :(得分:8)

以下是解决此问题的两种方法。第一个使用常见的bit-twiddling技术,如果仔细优化可以击败硬件划分。另一个用乘法替换除法,类似于gcc执行的优化,并且是最快的。最重要的是,如果第二个参数是常量,试图避开%运算符没有多大意义,因为gcc已经覆盖了它。 (也可能是其他编译器。)

以下函数基于xx的基数32位数之和相同(mod 31)的事实。这是正确的,因为321 mod 31,因此32的任何力量都是1 mod 31。因此,基数为32的数字中的每个“数字”位置将数字* 1贡献给mod 31总和。并且很容易得到base-32表示:我们一次只取五位。

(与此答案中的其他功能一样,它仅适用于非负x)。

unsigned mod31(unsigned x) {
  unsigned tmp;
  for (tmp = 0; x; x >>= 5) {
    tmp += x & 31;
  }
  // Here we assume that there are at most 160 bits in x
  tmp = (tmp >> 5) + (tmp & 31);
  return tmp >= 31 ? tmp - 31 : tmp;
}

对于特定的整数大小,您可以展开循环并且很可能击败分区。 (请参阅@chux's answer了解将循环转换为O(log bits)次操作而不是O(bits)的方法更难以击败gcc,这可避免在被除数为已知常数时进行除法在编译时。

在使用无符号32位整数的非常快速的基准测试中,天真的展开循环耗时19秒,基于@ chux答案的版本仅用了13秒,但gcc的x%31耗时9.7秒。强制gcc使用硬件除法(通过使除法非常数)花费23.4秒,并且如上所示的代码花费25.6秒。这些数字应该用几粒盐。时间用于使用i%31在我的笔记本电脑上为i的所有可能值计算-O3 -march=native

gcc通过用基本上64位乘法乘以常数的倒数后跟右移来避免32位除以常数。 (实际的算法可以做更多的工作来避免溢出。)该过程在20多年前的gcc v2.6中实现,描述该算法的论文可以在gmp site上找到。 (GMP也使用这个技巧。)

这是一个简化版本:假设我们想为某些无符号32位整数n // 31计算n(使用pythonic //来表示截断的整数除法)。我们使用“神奇常数”m = 232 // 31,即138547332。现在很明显,任何n

m * n <= 232 * n/31 < m * n + n ⇒ m * n // 232 <= n//31 <= (m * n + n) // 232

(这里我们使用a < b然后floor(a) <= floor(b)的事实。)

此外,由于n < 232m * n // 232(m * n + n) // 232是相同的整数或两个连续的整数。因此,这两者中的一个(或两个)是n//31的实际值。

现在,我们真的想要计算n%31。所以我们需要将(假定的)商乘以31,然后从n中减去该商。如果我们使用两个可能的商中较小的一个,可能会发现计算的模数值太大,但它只能太大了31。

或者,把它放在代码中:

static unsigned long long magic = 138547332;
unsigned mod31g(unsigned x) {
  unsigned q = (x * magic) >> 32;
  // To multiply by 31, we multiply by 32 and subtract
  unsigned mod = x - ((q << 5) - q);
  return mod < 31 ? mod : mod - 31;
}

gcc使用的实际算法通过使用基于乘以237//31 + 1的稍微更准确的计算来避免最后的测试。这总是产生正确的商,但代价是一些额外的移位并增加以避免整数溢出。事实证明,上面的版本稍微快一些 - 在与上面相同的基准测试中,只用了6.3秒。


其他基准功能,完整性:

天真的展开循环

unsigned mod31b(unsigned x) {
  unsigned tmp = x & 31; x >>= 5;
  tmp += x & 31; x >>= 5;
  tmp += x & 31; x >>= 5;
  tmp += x & 31; x >>= 5;
  tmp += x & 31; x >>= 5;
  tmp += x & 31; x >>= 5;
  tmp += x & 31;

  tmp = (tmp >> 5) + (tmp & 31);
  return tmp >= 31 ? tmp - 31 : tmp;
}

@ chux的改进,略微优化

static const unsigned mask1 = (31U << 0) | (31U << 10) | (31U << 20) | (31U << 30);
static const unsigned mask2 = (31U << 5) | (31U << 15) | (31U << 25);
unsigned mod31c(unsigned x) {
  x = (x & mask1) + ((x & mask2) >> 5);
  x += x >> 20;
  x += x >> 10;

  x = (x & 31) + ((x >> 5) & 31);
  return x >= 31 ? x - 31: x;
}

答案 1 :(得分:5)

[Edit2]下面的表现说明

只有1 if条件的尝试。

这种方法是O(log2(sizeof unsigned))。如果代码使用uint64_t,运行时间将增加1组ands / shifting / add而不是两倍的循环方法。

unsigned mod31(uint32_t x) {
  #define m31 (31lu)
  #define m3131 ((m31 << 5) | m31)
  #define m31313131 ((m3131 << 10) | m3131)

  static const uint32_t mask1 = (m31 << 0) | (m31 << 10) | (m31 << 20) | (m31 << 30);
  static const uint32_t mask2 = (m31 << 5) | (m31 << 15) | (m31 << 25);
  uint32_t a = x & mask1;
  uint32_t b = x & mask2;
  x = a + (b >> 5);
  // x = xx 0000x xxxxx 0000x xxxxx 0000x xxxxx

  a = x & m31313131;
  b = x & (m31313131 << 20);
  x = a + (b >> 20);
  // x = 00 00000 00000 000xx xxxxx 000xx xxxxx

  a = x & m3131;
  b = x & (m3131 << 10);
  x = a + (b >> 10);
  // x = 00 00000 00000 00000 00000 00xxx xxxxx

  a = x & m31;
  b = x & (m31 << 5);
  x = a + (b >> 5);
  // x = 00 00000 00000 00000 00000 0000x xxxxx

  return x >= 31 ? x-31 : x;
}

[编辑]

第一个加法方法将7个并行的7个组合并。随后的加法使7组进入4,然后是2,然后是1.最后的7位和然后继续将其上半部分(2位)加到其下半部分(5位)。然后代码使用一个测试来执行最终的“mod”。

此方法可扩展unsigned更宽,至少uint165_t log2(31 + 1)*(31 + 2)。通过它,需要更多的代码。

请参阅@rici以获得一些好的优化。仍建议在uint32_t之类的转变中使用unsigned31UL31U << 15,因为unsigned 31U可能只有16位长。 (2014年在嵌入式世界中流行的16位int)。


[EDIT2]

除了让编译器使用其优化器之外,另外两种技术可以提高性能。这些是更小的客厅技巧,取得了适度的改善。请注意YMMV,这适用于32位unsigned

使用查找最后modulo的表格提高了10-20%。使用unsigned t表而不是unsigned char t也有点帮助。事实证明,表长度,首先预计需要2 * 31,只需要31 + 5。

使用局部变量而不是总是调用函数参数令人惊讶地帮助了。可能是我的gcc编译器的弱点。

找到未显示的非分支解决方案,以替换x >= 31 ? x-31 : x。但是他们的编码复杂性更高,性能也更慢。

总而言之,这是一项有趣的运动。

unsigned mod31quik(unsigned xx) {
  #define mask (31u | (31u << 10) | (31u << 20) | (31u << 30))
  unsigned x = (xx & mask) + ((xx >> 5) & mask);
  x += x >> 20;
  x += x >> 10;
  x = (x & 31u) + ((x >> 5) & 31u);

  static const unsigned char t[31 * 2 /* 36 */] = { 0, 1, 2, 3, 4, 5, 6,
      7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24,
      25, 26, 27, 28, 29, 30, 0, 1, 2, 3, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
      0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 };
  return t[x];
}

答案 2 :(得分:2)

如果你想得到除以分母d的模数,使得d = (1 << e) - 1 e是指数,你可以使用{{1}的二进制展开的事实是每个1/d个数字设置位的重复分数。例如,ee = 5d = 31

rici’s answer类似,此算法有效地计算1/d = 0.0000100001...的基数 - (1 << e)数字的总和:

a

你可以展开这个循环,因为分母和分子中的位数都是常数,但让编译器这样做可能更好。当然,您可以将uint16_t mod31(uint16_t a) { uint16_t b; for (b = a; a > 31; a = b) for (b = 0; a != 0; a >>= 5) b += a & 31; return b == 31 ? 0 : b; } 更改为输入参数,将5更改为根据该值计算的变量。

答案 3 :(得分:1)

您可以使用连续的加法/减法。没有其他技巧,因为31是一个素数,可以看出数字N的模数是什么模型31你必须除以并找到余数。

int mode(int number, int modulus) {
    int result = number;

    if (number >= 0) {
         while(result > modulus) { result = result - modulus;}
    } else {
         while (result < 0) { result = result + modulus;)
    }
}

答案 4 :(得分:1)

int mod31(int a){
    while(a >= 31) {
        a -= 31;
    }
    return a;
};

如果a > 0有效,但我怀疑它会比%运算符更快。