深坑待填!
本文翻译自这篇文章,原作者Sean Eron Anderson. 如果有翻译错误的地方,欢迎指正。另外,由于年代久远,文中给出的链接可能已经失效。
UPDATE 2016/09/30: 继续翻译,并改进已翻译文本
开头语
本文所有代码片段皆属公有(除非特别指明)——您想怎么用就怎么用。所有的文本皆属Sean Eron Anderson版权所有(1997-2005)。本着希望帮助他人的想法,我将代码及其解释公之于众,但若要这些代码用于特定目的,请记住我并未明显或隐含地保证其可用和适用性。截止于2005年五月5日,所有代码均经过了完整的测试,并经上千人阅览。此外,卡内基·梅隆大学计算机学院院长Randal Bryant教授使用他的轮子uclid代码测试系统进行了测试,测试内容几乎涵盖了所有方面。他没有覆盖到的情况,我已经在32位机器上针对所有可能输入补充测试了一遍。对于第一个找到我代码里正当bug的人,我将给予他10美元的奖金(通过支票或PayPal)。如果将这笔钱转交慈善机构的话,增至20美元。
关于操作统计的说明
统计本文算法中操作数量的时候,所有C语言运算符皆看做是一次操作。不涉及写入内存的中间过程,不在统计范围内。当然,这种统计方式只能是对真实的机器指令和CPU时间的一个近似。在此假定所有的操作耗费相同的时间。这虽然与真实情况不符,但的确是CPU发展努力的方向。一个代码样本在不同系统上运行时间会有很多细微的差别,可能是缓存大小、内存带宽、指令集等等因素造成的。总之,拿去跑一跑是确定方法之间快慢的最好方法,所以在实际机器上测试的时候,我所说的仅供参考。
获取一个整数的正负号
1 | int v; //我们想知道v的符号 |
1 | // CHAR_BIT 代表一字节(byte)是多少位(bit)(一般是8) |
最后一个表达式,对于32位整数求得sign = v >> 31
,这比直接求法sign = -(v < 0)
少了一个操作。该方法之所以可行,是因为当我们把有符号整数向右移位时,最左边一位的值被复制到了其他位上。对一个有符号整数来说,最左边一位是1表示是负数,其他情况是0;所有位上全是1代表-1
. 可惜的是,上述行为是与架构相关的。(译注:带符号数负数的左移在C99标准中属于未定义行为)
另外,如果你希望sign
的结果是1
或-1
的话:
1 | sign = +1 | (v >> (sizeof(int) * CHAR_BIT - 1)); // v < 0得到 -1, 其余情况是 +1 |
其次,你希望结果是-1
, 0
或1
的话:
1 | sign = (v != 0) | -(int)((unsigned int)((int)v) >> (sizeof(int) * CHAR_BIT - 1)); |
此外,如果你想知道一个数是不是非负的,返回结果是+1
或者0
, 使用:
1 | sign = 1 ^ ((unsigned int)v >> (sizeof(int) * CHAR_BIT - 1)); // v < 0得到0, 其余是1 |
作者注:
- 2003年3月7日,Angus Duggan指出1989 ANSI C标准规定有符号整型的右移运算结果取决于具体实现,因此在某些系统上靠右移的方法可能失效。
- 为了使代码可移植性更好,2005年9月28日,Toby Speight建议我使用 CHAR_BIT 而不是指定一个字节8位长。2006年3月4日,Angus推荐了上面的使用了类型转换的方法,它可移植性更好。
- 2009年9月12日,Rohit Garg给出了检测非负数的方法。
检测两个整数是否异号
1 | int x, y; // 待检测的两个数 |
1 | bool f = ((x ^ y) < 0); // 当且仅当x与y符号相反的时候为真 |
作者注:
- 2009年11月26日的时候Manfred Weis建议我加上这个条目。
不利用分支来计算整数的绝对值
1 | int v; // 我们现在要求v的绝对值 |
1 | r = (v + mask) ^ mask; |
另一个专利所有的变体:
1 | r = (v ^ mask) - mask; |
某些CPU没有绝对值指令(或者编译器未能使用到它)。在分支指令开销很大的机器上,上面的表达式能够比直接求法r = (v < 0) ? -(unsigned)v : v
快很多,尽管操作的数量一样。
作者注:
- 2003年3月7日,Angus Duggan指出1989 ANSI C标准规定有符号整型的右移运算结果取决于具体实现,因此在某些系统上靠右移的方法可能失效。我拜读了ANSI C, 其中并没有要求将数值表示为补码,所以说这也可能是方法失效的原因(在那些逐渐退出历史舞台的老爷机上仍然采用反码表示)。
- 2004年3月14日,Keith H. Duggar告诉了我上面的变体,比我刚开始想出来的那个
r=(+1|(v>>(sizeof(int)*CHAR_BIT-1)))*v
要好,因为少用了个乘法。不幸的是,那个方法于2000年6月6日由Vladimir Yu Volkonsky在美国申请了专利,归属于Sun Microsystems. - 2006年8月13日,Yuriy Kaminskiy告诉我专利可能失效因为在专利申请之前它就已经被发布出去了,比如早在1996年11月9日Agner Fog就已经发表的How to Optimize for the Pentium Processor. Yuriy还告诉我那篇文档在1997年翻译成了俄文,而Vladimir可能读过。此外,互联网档案也有个指向它的旧链接。
- 2007年1月30日,Peter Kankowski对我分享了他受Microsoft Visual C++编译器输出启发而想到的绝对值版本。该方法被拿来作为本题的主要解决方案。
- 2007年12月6日,Hai Jin抱怨说结果是有符号的,所以计算大多数负数绝对值的时候结果还是负的。
- 2008年4月15日,Andrew Shapira指出那个直接求法可能会溢出,因为缺少一个
(unsigned)
的类型转换。为了尽可能满足移植性要求,他给我的改进求法是(v < 0) ? (1 + ((unsigned)(-1-v))) : (unsigned)v
. - 2008年7月9日,Vincent Lefèvre引用ISO C99标准说服了我,让把上面那个删掉,因为即使不是使用补码的机器
-(unsigned)v
也能得到正确的结果。计算-(unsigned)v
的时候,首先加上2**N
来将负值v
转化成无符号值,得到v
的补码表示(记做U
)。然后,取U
的相反数,得到结果:-U = 0 - U = 2**N - U = 2**N - (v + 2**N) = -v = abs(v)
.
译注:2**N 表示 $2^N$.
不利用分支来计算两个整数的最大(max)最小(min)值
1 | int x; |
1 | r = y ^ ((x ^ y) & -(x < y)); // min(x, y) |
这个世界上还有使用条件分支非常耗费时间的老爷机,而且没有条件移动指令。在这些机器上上面的方法可能会比直接求法r = (x < y) ? x : y
要快,尽管位运算解法涉及了两个以上的指令(虽然一般说来,直接解法是最好的)。 解法证明如下:如果x < y
, 那么-(x < y)
的各二进制位就全是1, 所以r = y ^ ((x ^ y) & -(x < y)) = y ^ ((x ^ y) & ~0 = y ^ x ^ y = x
. 另外,如果x >= y
, 那么-(x < y)
就全是0, 所以r = y ^ ((x ^ y) & 0) = y
. 在某些机器上,计算(x < y)
要得到0或1的话需要分支指令,因此该解法不具备任何优势。
求最大值的话,使用:
1 | r = x ^ ((x ^ y) & -(x < y)); // max(x, y) |
快而脏的解法
如果你知道INT_MIN <= x - y <= INT_MAX
的话,那你就可以用下面这种解法,由于它只需要计算一次(x - y)
所以会快一点。
1 | r = y + ((x - y) & ((x - y) >> (sizeof(int) * CHAR_BIT - 1))); // min(x, y) |
由于1989 ANSI C未规定有符号数右移的结果,所以该解法不可移植。如果溢出时抛出异常,那么减法时x
与y
的值就是无符号型或者被转换成无符号型来防止不必要的异常抛出。然而这里右移运算需要一个有符号的操作数,使得右移负数时填充1,所以这里转换成有符号整型。
作者注:
- 2003年3月7日,Angus Duggan指出右移的可移植性问题。
- 2005年5月3日,Randal E. Bryant警告我说需要一个
INT_MIN <= x - y <= INT_MAX
的前提条件,并给出了一个慢而脏的修改版本。以上只在这个快而脏的解法里考虑了。 - 2005年7月6日,Nigel Horspoon观察到在奔腾上,由于gcc对
(x < y)
的求值问题,它生成了和直接解法一样的代码。 - 2008年7月9日,Vincent Lefèvre指出
r = y + ((x - y) & -(x < y))
里的减法有潜在的溢出风险(这是我前一版的代码)。 - 2009年6月2日,Timothy B. Terriberry建议我使用异或而不是加减来避免溢出的危险。
检测一个数是不是2的次幂
1 | unsigned int v; // 要检测的数是v |
1 | f = (v & (v - 1)) == 0; |
注意0
并不是2的多少次幂,因此做如下修改:
1 | f = v && !(v & (v - 1)); |
符号扩展
从给定位宽扩展
符号扩展对于内建类型来说是自动的,比如从char
到int
. 但是,假设你有一个以b
位补码表示的有符号整数x
, 你想把x
转换成int
型的,此时存储空间大于b
位。只是简单地复制的话,当x
是正数的时候还可以,是负数的时候就不对了,必须进行符号扩展。例如,假设我们只用4位来存储数字,那么-3
表示成二进制就是1101
. 如果用8位来存,那么就是11111101
. 当扩展一个负数的时候需要将扩展的高位全赋为1, 这就是符号扩展。在C语言里,符号扩展就是小事一桩,因为可以在struct
或union
里使用位域。例如,要将5位长整数扩展到int
型:
1 | int x; // 将它从5位长转换到int |
1 | struct {signed int x:5;} s; |
下面是一个使用了相同语言特性的C++模板函数,仅用一个操作,就能从b位长扩展(尽管编译器生成的肯定不止这么点):
1 | template <typename T, unsigned B> |
作者注:
- 2005年5月2日,John Byrd发现了代码里一处拼写错误(都是HTML的锅)。
- 2006年3月4日,Pat Wood指出ANSI C标准要求位域需要关键字
signed
来表示有符号数,否则其符号未定义。
从不定长位宽扩展
有时我们需要扩展的数并不知道它的位长b
. 或者我们用的是一门没有位域特性的语言,比如 Java.
1 | unsigned b; // 表示x的位长 |
上面的代码需要四次操作,但如果位宽给定的话,我们就只需要两个高效率的操作(假设高位已经全是0)。
一个牺牲了可移植性的稍快点的方法如下,这种方法并不要求x
的b
位以上的数字是0:
1 | int const m = CHAR_BIT * sizeof(x) - b; |
作者注:
- 2004年6月13日,Sean A. Irvine希望我能在本页加上这些符号扩展的方法。他给了我最初的版本
m = (1 << (b - 1)) - 1; r = -(x & ~m) | x
, 后来被我优化成了m = 1U << (b - 1); r = -(x & m) | x
. - 然而2007年5月11日,Shay Green给了我上面解法里的版本,比我的还少一次操作。
- 2008年10月15日,Vipin Sharma给我改进建议,考虑到了
x
的高位可能有1
的情况。 - 2009年12月31日,Chris Pirazzi, 建议我加上下面要讲的那种解法。
从不定长位宽扩展,3次操作
下面讲的这个解法可能在某些机器上较慢,因为它需要乘除法。该版本需要4次操作。如果你知道位宽b
比1大,请使用r = (x * multipliers[b]) / multipliers[b]
完成这种类型的符号扩展,它仅需3次操作,一次查表。
1 | unsigned b; // 表示x的位长 |
下面的这个变体并不可移植,但在使用算数右移的架构上,能保留符号,应该能快一点。
1 | const int s = -b; // 或者: sizeof(x) * CHAR_BIT - b; |
作者注:
- 2005年5月3日Randal E. Bryant发现了早期版本里的一个bug(把
multipliers[]
用作了divisors[]
),导致了当x=1,b=1
的时候出错。
选择性地置位清零,不利用分支
1 | bool f; // 表示条件 |
作者注:
- 在某些架构上,没有条件分支要强于使用多两倍的操作数量来计算分支。例如,据非正式测试显示,在AMD Athlon™ XP 2100+上能快上5-10%. Intel Core 2 Duo运行超标量版比普通版快了约16%.
- 2003年12月11日,Glenn Slayden告诉了我第一个版本。
- 2007年4月3日,Marco Yu告诉了我超标量版并于两天后指出了两个拼写错误。
选择性地取负,不利用分支
如果你希望仅当条件为假的时候取负,下面是不涉及条件分支的版本:
1 | bool fDontNegate; // 标识我们什么时候不对v取反 |
1 | r = (fDontNegate ^ (fDontNegate - 1)) * v; |
如果你希望当条件为真时操作:
1 | bool fNegate; // 标识我们什么时候对v取反 |
1 | r = (v ^ -fNegate) + fNegate; |
作者注:
- 2009年6月2日,Avraham Plotnitzky建议我加上第一个版本。
- 我又想避免使用乘法,因此在2009年6月8日想出了第二个版本。
- 2009年11月26日,Alfonso De Gregorio指出我丢了几个括号,并收到了bug奖励。
根据掩码合并两个数
1 | unsigned int a; // 合并未屏蔽位 |
1 | r = a ^ ((a ^ b) & mask); |
相比合并两个数的直接解法,我的比它少了一个操作。如果掩码是常量的话,这种方法不占任何优势。
作者注:
- 2006年2月9日,Ron Jeffery给了我这个。
统计数位中1的个数
简单粗暴的解法
1 | unsigned int v; // 待统计的数 |
这种解法每一位一次循环,直到没有1
. 所以对一个只有最高位是1
的32位数来说,它会循环32次。
查表法
1 | static const unsigned char BitsSetTable256[256] = |
1 | c = BitsSetTable256[v & 0xff] + |
或者:
1 | unsigned char * p = (unsigned char *) &v; |
1 | // 用算法来初始化数组 |
作者注:
- 2009年7月14日Hallvard Furuseth建议我使用宏来生成表。
Brian Kernighan的方法
1 | unsigned int v; |
这种方法的循环次数和1
的个数一样多。所以如果有一个只有最高位是1
的数,只会循环一次。
作者注:
- 该方法发表于1988, the C Programming Language 2nd Ed (作者Brian W. Kernighan和Dennis M. Ritchie). 书中练习2-9提到了这种方法。
- 2006年4月19日,Don Knuth指出这种方法是由Peter Wegner在 CACM 3 (1960), 322第一次发表。(同时也由Derrick Lehmer指出该方法在1964年Beckenbach编过的一本书里发表过。)
针对14, 24, 或32位长数字,使用64位指令集
1 | unsigned int v; |
该方法需要能快速求余的64位CPU才能高效率。14位长的只需要3次操作;24位的需要10次;32位15次。
作者注:
- Rich Schroeppel最初原创了针对9位的求法,类似于我这里的14位的方法;详见Beeler, M., Gosper, R. W., and Schroeppel, R. HAKMEM. MIT AI Memo 239, Feb. 29, 1972.的Programming Hacks部分。它启发了Sean Anderson(译注:作者自己), 并想出了上面的几种方法。
- 2005年5月3日,Randal E. Bryant找出了一堆bug.
- 2007年2月1日,Bruce Dawson对12位方法做了微调,使之变成了现在的14位版本。
并行方法
1 | unsigned int v; // 要统计v(32位数值) |
数组B
表示为二进制,就是:
1 | B[0] = 0x55555555 = 01010101 01010101 01010101 01010101 |
对于大点的数据,我们可以延长魔法数字B
和S
来适配。如果数字有k
位,数组S
和B
就应该有$\lceil \lg(k) \rceil$个元素,而且我们要写出关于c
的同样多个数的表达式。对于一个32位长的v
, 进行了16次操作。
对于一个32位整数v
的最佳解法是:
1 | v = v - ((v >> 1) & 0x55555555); // 用v来存储中间值 |
该解法只用了12次操作,与查表法的相同,并避免使用额外内存和缓存未命中表的情况。该解法混合了上面并行法和更前面的使用乘法的解法(使用64位指令集的那个解法),然而它并没有用到64位指令集。统计在并行部分完成,计数求和由乘以0x1010101
并右移24位完成。
一个最佳通用解法如下,它最多支持到128位(参数化表示为T
):
1 | v = v - ((v >> 1) & (T)~(T)0/3); // 中间值 |
作者注:
- 更多信息见Ian Ashdown的贴子。
- 最佳解法是2005年10月5日由Andrew Shapira告诉我的;他在Software Optimization Guide for AMD Athlon™ 64 and Opteron™ Processors的187-188页找到了它。
- 2005年12月14日Charlie Gordon给了我一个能优化掉一次操作的建议。
- 2005年12月30日Don Clugston从中整理出了三种方法。
- 我接受了建议后又犯了几个拼写错误,于2006年1月8日由Eric Cole指出。此后,他给了上面那个通用解法的建议。
- 2007年4月5日,Al Williams告知我在最开始的方法里有一句冗余代码。
从最高位到指定位
下面给出求从最高位到指定位1
的个数(rank)的解法:
1 | uint64_t v; // 待计算的数v |
作者注:
- 2009年11月21日,Juha Järvi给了我这个下面问题的镜像问题。
已知rank求出位数
下面的64位代码计算出从左至右第r
个1
. 也就是说,从最高位向右统计1的个数,直到得到rankr
, 返回对应的这个位。如果给出的rank超出了范围,返回64
. 代码可以针对32位或者从左往右计算做出修改。
1 | uint64_t v; // 待计算的数v |
如果分支在你的CPU上速度很快的话,取消使用注释里的条件语句进行替换。
奇偶校验
译注:信息是以比特流的方式传输的,类似01000001
. 在传输过程中,有可能会发生错误,比如,我们存储了01000001
,但是取出来却是01000000
,即低位由0变成了1。为了检测到这种错误,我们可以通过“奇偶校验”来实现。假如,我们存储的数据是一个字节,8个比特位,那我们就可以计算每个字节比特位是1
的个数,如果是偶数个1
,那么,我们就把第九个位设为1
,如果是奇数个1
,那么就把第九个位设为0
,这样连续9个字节比特位为1
的位数肯定是奇数。这种方法叫做“奇校验”,“偶校验”和此类似。当然,在实际应用中,也可以把一个字节的前7位作为数据位,最后一个为作为校验位。
简单粗暴的解法
1 | unsigned int v; // 待检验的数v |
该解法使用了类似于前述Brian Kernigan解法的思路,消耗时间与二进制1
的数量成正比。
查表法
1 | static const bool ParityTable256[256] = |
作者注:
- 2005年5月3日Randal E. Bryant支持我加上那个很直接的解法(最后那个变体)。
- 2005年9月27日,Bruce Rawles找到了几个表名的拼写错误,并因此收到了$10的找bug奖金。
- 2006年10月9日,Fabrice Bellard给了我上面的32位长的版本,它只需一次查表,原来的版本需要4次查表,更慢。
- 2009年7月14日Hallvard Furuseth建议我使用宏来生成表。
使用64位指令集的乘法和求余
1 | unsigned char b; // 待计算的数b |
上面的方法大概需要4次操作,但只对整字节有效。
使用一次乘法
以下方法能仅用8次操作处理一个32位长的数值,而且只用了一次乘法。
1 | unsigned int v; // 32-bit word |
对于64位长的,8次操作也足够:
1 | unsigned long long v; // 64-bit word |
作者注:
- Andrew Shapira想出了这个方法并于2007年9月2日给了我。
并行法
1 | unsigned int v; |
以上方法需要大概9次操作,对32位的有效。对于整字节也可以删掉unsigned int v
后面的两行来优化到只用5次操作。该方法首先使用移位和异或对v的每段(每段8bit)进行处理,异或后只取后4个bit, 然后二进制数0110 1001 1001 0110
(16进制是0x6996
)右移这么多位。这就好像是拿v
的后四位去小型的16位奇偶表里去查。v
的奇偶结果以二进制1
的形式保存并返回。
作者注:
- 感谢Mathew Hendry于2002年12月15日告诉我的移位-查表方法。优化后能少两次操作并且只用了移位和异或。
交换两个数的值
使用加减法
译注:不要把右值往a
, b
里代入。
1 |
|
该方法能不使用额外的临时变量来交换a
和b
的值。开始的那句是检查a
与b
是否使用了同一块内存空间,是的话那么该方法结果就不对了(编译器可能会把这个检查优化掉)。如果你开启了溢出异常检测,那么要传递无符号值以避免触发异常。下面的异或方法在某些机器上可能会快一点。不要对浮点数也这么操作,除非你操作的是它们的二进制值。
作者注:
- 2007年6月12日Sanjeev Sivasankaran建议我加上这一条。
- 2008年7月9日Vincent Lefèvre指出它有潜在的溢出风险。
使用异或
译注:同上。
1 |
这是一种很古老的,不使用额外临时变量的交换技巧。
作者注:
- 2005年1月20日,Iain A. Fleming指出该方法当作用于同一内存空间的时候失效,比如我当
i==j
的时候SWAP(a[i], a[j])
. 所以为避免这种情况的发生,把宏定义改成(((a) == (b)) || (((a) ^= (b)), ((b) ^= (a)), ((a) ^= (b))))
. - 2009年7月14日,Hallvard Furuseth告诉我说,在某些机器上
(((a) ^ (b)) && ((b) ^= (a) ^= (b), (a) ^= (b)))
会更快一点,因为(a) ^ (b)
的值能重复利用。
交换指定的某几个二进制位
1 | unsigned int i, j; // 给定位置 |
举个例子,假如我们有个数b = 00101111
(二进制表示),我们想把它里面从i = 1
开始的连续n = 3
位与从j = 5
开始的连续n
位交换,其结果应该是r = 11100011
(二进制表示)。
该方法的思路与异或法相似。x
用于保存我们想要交换的那几位,然后这几位的值被赋值为它们自己与x
的异或结果。当然,如果序列超出范围其结果未定义。
作者注:
- 2009年7月14日,Hallvard Furuseth建议我把
1 << n
改成1U << n
来强行赋值为无符号型来避免对意外地有符号数移位。
倒置二进制位
直接解法
1 | unsigned int v; // 待处理的数v |
作者注:
- 2004年10月15日,Michael Hoisie指出最初版本有一个bug.
- 2005年5月3日Randal E. Bryant建议我删掉一个冗余操作。2005年5月18日Behdad Esfabod给了我一个小建议,它能减少一次循环。
- 2007年2月6日,Liyong Zhou给了我点建议,使得循环只在
v
不是0的时候运行,而不是对每一位都循环一次。
查表法
1 | static const unsigned char BitReverseTable256[256] = |
第一个方法要用17次操作,第二种需要12次,如果你的CPU能够快速读取和存储数据的话。
作者注:
- 2009年7月14日Hallvard Furuseth建议我使用宏来生成表。
倒置一比特,仅用3次操作(使用64位的乘法和求余)
1 | unsigned char b; // reverse this (8-bit) byte |
乘法操作生成了五分相互独立的拷贝,每份8位,并合并成一个64位值。将该值分为每十位一组,与操作选出哪些位在组中正确的(倒置)位上。乘法和与运算从原数据拷贝到这里来,所以每个组中一定会有一个原数据。比特位的倒置位置与它们在组中的相对位置相同。最后,求得模2^10 - 1
的余数,即能把每组中的选中的比特位聚合到一起。它们相互不会覆盖,所以求余操作的底层操作就像逻辑或操作一样。
该方法见于Beeler, M., Gosper, R. W., and Schroeppel, R. HAKMEM. MIT AI Memo 239, Feb. 29, 1972.的Programming Hacks部分。
倒置一比特,仅用4次操作(使用64位的乘法,不使用除法)
1 | unsigned char b; // reverse this byte |
下面就是该方法的原理展示,使用a
,b
,c
,d
,e
,f
,g
,h
来表示该比特的各个位。注意第一次乘法是如何将这个数拷贝多份的。以及最后一次乘法是如何将这几个位合并到从右往左数第五个比特中去的。
1 | abcd efgh (-> hgfe dcba) |
最后两步可以合并,因为某些处理器的寄存器可以按照字节来读写;只用乘法,然后寄存器存储高位的32位,再取低八位,这样就只要6次操作。
该方法于2001年7月13日由Sean Anderson创造而出。
倒置一比特,仅用7次操作(不使用64位)
1 | b = ((b * 0x0802LU & 0x22110LU) | (b * 0x8020LU & 0x88440LU)) * 0x10101LU >> 16; |
注意要把结果赋值给或转换到unsigned char
以去除不需要的垃圾位。
作者注:
- 2001年7月13日,该方法由Sean Anderson创造而出。
- 2002年1月3日,Mike Keith指出并修正了拼写错误。
倒置N位,并行法,使用 5*lg(N) 次操作
1 | unsigned int v; // 32-bit word to reverse bit order |
下面的变体依然是$O(\lg N)$的,不过它需要更多次操作来倒置v
. 它好在即时计算常量来减少内存使用。
1 | unsigned int s = sizeof(v) * CHAR_BIT; // bit位数,必须是2的次幂 |
以上方法非常适合于N很大的情形。对于64位或以上的整数,还得按照套路加几行,否则只有低32位被倒置。
作者注:
- 更多内容详见Dr. Dobb’s Journal 1983, Edwin Freed关于Binary Magic Numbers的文章。
- 2005年9月13日,Ken Raeburn推荐给我第二种变体。
- 2006年3月19日,Veldmeijer提出第一种方法的最后一行可以不使用与运算。
求余除法
不使用除法,计算关于 1<<s 的余数
1 | const unsigned int n; // 被除数 |
大部分程序员很早就知道这个技巧了,为了完整性我将其添加进来。
不使用除法,计算关于 (1<<s) - 1 的余数
1 | unsigned int n; // 被除数 |
求关于 $2^N-1$ 的余数需要 $5 + (4 + 5 \lceil {N \over s} \rceil) \cdot \lceil \lg{N \over s} \rceil$ 次操作,其中N
是被除数的位数。也就是说,时间复杂度为$O(\lg N)$.
作者注:
- 该方法于2001年8月15日由Sean Anderson想出。
- 在2004年6月17日Sean A. Irvine指正之前,我错误地评论说可以在最末尾使用
m = ((m + 1) & d) - 1;
. - 2005年4月25日,Michael Miller指出代码中一处拼写错误。
不使用除法,并行计算关于 (1<<s) - 1 的余数
1 | // The following is for a word size of 32 bits! |
该方法求关于 $2^N-1$ 的余数至多需要 $O(\lg N)$ 时间,其中N
是被除数位长(以上代码是针对32位的)。所需操作次数至多是 $12 + 9 \lceil \lg N \rceil$. 如果你能在编译器知道除数的话,就能去掉表,只需提取相关条目并做循环展开。该方法很容易能扩展到更长位宽。