我知道2的幂的模数可以使用按位运算符
来计算 x % 2^n == x & (2^n - 1).
但我想知道是否存在任何通用的按位算法来查找任何数的模数不是2的幂。例如,
7%5
提前谢谢你。
答案 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
。