C - 针对模数的按位运算的算法,对于非2的幂次数

时间:2018-04-20 20:53:37

标签: c bit-manipulation bitwise-operators

我知道2的幂的模数可以使用按位运算符

来计算
  x % 2^n == x & (2^n - 1).

但我想知道是否存在任何通用的按位算法来查找任何数的模数不是2的幂。例如,

 7%5 

提前谢谢你。

2 个答案:

答案 0 :(得分:6)

不,在没有实际划分的情况下,没有找到除法余数的通用方法。

由于二进制表示,两个幂是一个例外,它允许你使用移位除以2。同样的原则在于,只需将数字从末尾删除,就可以将十进制数除以10的幂。

显然,没有什么可以阻止你编码division using bit operations。您还需要对减法进行编码,因为算法要求将其作为“基本操作”。你可以想象,这将是非常缓慢的。

答案 1 :(得分:6)

有一对,特殊情况,包括5。

从16≡1(mod 5)开始,你可以做的一个技巧是将你的变量分成4位半字节,查找表中每个半字节的模数,并将这些值加在一起得到原始模数号。

该程序使用位域,表查找和添加。它也适用于模3或15,并且可以扩展到具有更大查找表的更大块。

#include <assert.h>
#include <stdio.h>
#include <stdint.h>
#include <stdlib.h>

typedef struct bitfield64_t {
  uint64_t b0 : 4;
  uint64_t b1 : 4;
  uint64_t b2 : 4;
  uint64_t b3 : 4;
  uint64_t b4 : 4;
  uint64_t b5 : 4;
  uint64_t b6 : 4;
  uint64_t b7 : 4;
  uint64_t b8 : 4;
  uint64_t b9 : 4;
  uint64_t b10 : 4;
  uint64_t b11 : 4;
  uint64_t b12 : 4;
  uint64_t b13 : 4;
  uint64_t b14 : 4;
  uint64_t b15 : 4;
} bitfield64_t;

typedef union pun64_t {
  uint64_t u;
  bitfield64_t b;
} pun64_t;

/* i%5 for i in [0,19].  The upper bound guarantees that nibble_mod5[a+b] is
 * valid whenever a<16 and b<5.
 */
const unsigned nibble_mod5[20] = {
  0, 1, 2, 3, 4, 0, 1, 2, 3, 4, 0, 1, 2, 3, 4, 0, 1, 2, 3, 4
};

unsigned add_mod5( const unsigned a, const unsigned b )
/* Returns (a + b) % 5, where
 *   a < 16
 *   b < 5
 */
{
  assert(a < 16);
  assert(b < 5);
  return nibble_mod5[a + b];
}

int main( const int argc, const char* argv[] )
{
  int64_t n;

  if ( argc != 2 ) {
    fprintf( stderr,
             "Call this program with an unsigned number as its argument.\n" );
    return EXIT_FAILURE;
  }

  if ( 1 != sscanf( argv[1], "%lld", &n ) || n < 0 ) {
    fprintf( stderr,
             "The argument must be an unsigned number.\n" );
    return EXIT_FAILURE;
  }

  const pun64_t p = { .u = (uint64_t)n };
  const unsigned result =
    add_mod5( p.b.b15,
    add_mod5( p.b.b14,
    add_mod5( p.b.b13,
    add_mod5( p.b.b12,
    add_mod5( p.b.b11,
    add_mod5( p.b.b10,
    add_mod5( p.b.b9,
    add_mod5( p.b.b8,
    add_mod5( p.b.b7,
    add_mod5( p.b.b6,
    add_mod5( p.b.b5,
    add_mod5( p.b.b4,
    add_mod5( p.b.b3,
    add_mod5( p.b.b2,
    add_mod5( p.b.b1,
    nibble_mod5[p.b.b0] )))))))))))))));

   printf( "%u\n", result );
   assert( result == n % 5 );
   return EXIT_SUCCESS;
}

为了找到bignum的模数,你可以利用16的任何幂与1模5一致的事实。因此,你的字大小 w 是2⁸,2ⁱ⁶,2³²或者2⁶⁴,你可以把你的bignum写为axw⁰+ a1W1 + a2w2 + ......a01⁰+ a111 + a212 + ...≡a0+ a 1 + a 2 + ...(mod 5)。这也是为什么任何数字的小数位数与模3或9的原始数字一致的原因:10≡1(mod 3)。

这也适用于3字节,5字节,15字节和17字节,16位字的因子为255和257,32位字的因子为65,535和65,537。如果你注意到这个模式,那是因为b²ⁿ=(bⁿ+ 1)(bⁿ-1)+ 1,其中b = 2,n = 2,4,8或16。

您可以将此方法的变体应用于任何n,使得您的块大小与-1(mod n)一致:交替加法和减法。它的工作原理是因为a0w⁰+a1w¹+ a2w2 + ...≡a0(-1)⁰+ a 1(-1)¹+ a 2(-1)²+ ...≡a0 - a 1 + a 2 - ...(mod n ),但是没那么有用,因为许多这样的n值都是梅森素数。它类似于你如何通过从右到左并加上,减去,加上和减去数字来获取任何小数的模数11,例如144≅4 - 4 +1≡1(mod 11)。就像数字一样,你可以使用五位块执行相同的技巧,因为32(如10)也与-1 modulo 11一致。

w w ²≡c(mod b)时,会出现另一个有用的特殊情况。然后你有一个δw⁰+ a1w1 + a2w2 + ......≡a0·1 + a1c + a2c + ......≡a0+ c(a 1 + a 2 + ...)(mod b)。这类似于10≡100≡1000≡...≡4(mod 6),所以任何数字都与其最后一位数加上其余数位之和的四倍,模数为6.计算可以是查找和每个字节加一个,乘以一个小常数乘以一个或两个位移。例如,要采用mod 20,您可以添加除最低位字节mod 20之外的所有字节,将和乘以256 mod 20 = 16,这只是左移4,然后添加最后一个字节。这可能非常方便:不计算给出1或0的余数的数字,这适用于模数为6,10和12的半字节,以及以这些值为模的字节和20,24,30,34,40,48,60,68 ,80,96,102,120,136,160,170,192,204和240。

如果数字可以表示为特殊情况的乘积,则可以使用中国剩余定理求解。例如,77 = 11×7,32≡-1 mod 11和8≡1mod 7,因此你可以找到余数除以11和7,它们确定余数除以77.大多数小素数都归为1以前讨论的特殊情况。

许多后来的RISC架构有硬件鸿沟但没有模数,并告诉程序员通过计算a%b来计算a-(a/b)*b。 ARM A64是目前使用最多的产品。如果您没有硬件部门,check out this answer。当base是一个小常量时,另一种方法的例子是here,并且在CISC架构中被广泛使用。

还有一个算法written by Sean Anderson in 2001 but probably discovered earlier来计算模数比2的幂小一号。它类似于我上面使用的技术,但依赖于位移并且可以扩展到任何因子(1<<s)-1。这几乎就是你要找的东西!

通常,优化编译器应该使用最有效的方法在硬件上实现%。在您的示例中,任何体面的编译器都只会折叠常量并将7%5优化为2