Hacker‘s Delight, 黑客的喜悦,看标题似乎这本书是一部关于黑客的小说。其实这是Herry S. Warren, Jr.撰写的一部有关编程技巧的工具书。
当然,其中描述的技巧可以说是早期计算机黑客积累下的点滴。书中已提,黑客指的是以沉浸在计算机开发中以之乐的一群人,不是指入侵他人电脑的人。
这本书也承认书中技巧基本是一些一行代码即可囊括的小技巧,不是指搜索排序这样的算法,而Knuth的“计算机程序设计艺术”也在该书书评中被提及,称此书尚可与Knuth的书并列于书架,我想应该算作为在学习Knuth著作同时的一些补充,阅读Knuth的书不免要深入数学理论的研究,依然是其他算法书籍难于比肩的。
而这本书也在Knuth的“计算机程序设计艺术”vol 4A-7.1.3 中被提及,这也是我发现此书的原因,可见其值得一读。
基础部分
记号: & 位与
| 位或
^ 位异或
~X 位反
-X 负X
2**n 2的n次幂
2-1 操作最右端比特位
下式消去 x 最右侧比特“1” 如 x = 01011000 : 01011000 & (01010111) -> 01010000, 可用于判断是否数字是2的幂,if( (0 != x) && (0 == ( x & (x-1) ))) { //是2的幂 },
x& (x-1)
由上可导出, 下式(0-test)判断 x 是否是 2的幂减一(2^n-1) 如 00111111 -> 00000000, 或说消去最低位起连续比特“1”,如 01011011 -> 01010000,当最低位是“0”,则无变化,如01011100 -> 01011100
x& (x+1)
下式提取最右端比特“1” 如 x = 01011000 : 01011000 & ( 10101000 ) -> 00001000,
x& (-x)
下式提取最右端比特“0” 如 x = 01000011 : (01000011 + 1) & ( 10111101 ) -> 00000100,
(-x)& (x+1)
下式 用于构成尾部“0”串的掩码 ,如 01001100 -> 00000011
(-x)& (x-1)
-(x|-x)
(x& -x)
下式用于构成最右端“1”加尾部“0”串的掩码 ,如 01001100 -> 00000111
x^(x-1)
下式用于将尾部“0”串全变成“1” 如 01001100 -> 01001111
x | (x-1)
下式用于将最右部第一组连续“1”串变成“0”, 如 01001100 -> 01000000 , 01010100 -> 01010000
((x | (x-1))+1) & x
这个判据可以用于判断是一个非负整数是 否是两个 2的幂的差 (2**j - 2**k for j>= k>= 0)
上面公式都是两面的,如果要应用到 "0 - 1" 互换的情况,则 用 x-1 代替x+1,x+1 代替x-1,-x 代替 -(x+1),位或 (|)代替位与(&),位与( &)代替位或(|),x 和~X保持不变,转换后公式依然成立。如x& (x-1)转换为x|(x+1)后的公式,用于将最右边的比特 0 置 1,如 x = 10100111 : 10100111|(10101000) -> 10101111。
有一个简单的测试可判断一个已知函数是否能用一串加,减,与,或,非来实现。我们当然可以用其他组成基本列表的指令展开该函数,如固定数量的左移(相当于一串加法运算)或乘法。但是,我们要从该列表中排除不能被组合的指令。以下定理指出了这个测试方法。
定理A:一个words到words的映射函数能用word-parallel 加,减,与,或,非来实现,当且仅当结果的每个比特位仅依赖于其每个操作数本身所在比特位和右侧的比特位。
想象一下,如果只通过观察每个输入操作数最右端比特位来计算结果的最右端比特位,然后向左计算下一个比特位,以此类推。如果你成功计算到结果,那么该函数能被一串加,减,与,或,非来实现。如果不能通过从右到左的方式计算,则该函数不能被一串加,减,与,或,非来实现。
这个定理有趣的部分在于,其逆命题是那么简单地考察得到,即“由于加,减,与,或,非都能从右到左来计算,因而它们任意组合也具有同样属性”。为验证这个定理,我们下面用一个比较奇怪的函数式。
算式 1 : r2 = x2 | ( x0 & y1 )
对于32位变量,比特从右到左标记为0到31,由于计算结果的bit2是输入操作数bit[2-0]的结果,因而可以说,bit2是右到左可计算的。
第二行y左移一位和第三行x左移 2位做位与运算,和第一行x做位或运算,用bit2掩码位与,得到结果r2
x31 x30 ... x3 x2 x1 x0
x29 x28 ... x1 x0 0 0
y30 x29 ... y2 y1 y0 0
0 0 ... 0 1 0 0
0 0 ... 0 r2 0 0
算式 2 :s = x & -x
r = s + x
y = r | ( ( (x^r)>>2) / s )
这个算式输入是x,输出是y,用于获得比x更大的并和x有相同的比特“1”的数。它有啥用呢?要是你需要用整数代表集合做集合运算就可能要用上。
假定x的第i位是否为“1”表示对应元素是否在整数代表的集合内,那你可以用位或运算求并集,用位与运算求交集。而要是需要遍历所有的含k个元素的集合,可以用上这个算式,它的C代码实现如下。
unsigned snoob(unsigned x) { unsigned smallest, ripple, ones; // x = xxx0 1111 0000 smallest = x & -x; // s = 0000 0001 0000 ripple = x + smallest; // r = xxx1 0000 0000 ones = x ^ ripple; // 0001 1111 0000 ones = (ones >> 2)/smallest; // 0000 0000 0111 return ripple | ones; // xxx1 0000 0111 }
y = r | ( (x^r)>>(2 + ntz(x)) )
y = r | ( (x^r)>>(33 - nlz(s)) )
目前我所知的后导零计算的快速算法,使用de bruijn序列加上hash表查找。当然要是硬件支持前导零后导零计算就更好了,只是移植性恐怕会受影响。
2-2 更多逻辑运算组合 略
2-3 逻辑算术运算的不等式
( x ^ y ) <= ( x | y )
( x & y ) <= ( x == y )
以下是逻辑算术运算真值表,其运算排列顺序看似凌乱,其实是精心安排的。
x |
y | 0 | x&y | x&~y | x | ~x&y | y | x^y | x|y | ~(x|y) | x==y ~(x^y) |
~y | x|~y | ~x | ~x|y | ~(x&y) | 1 |
0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 |
0 | 1 | 0 | 0 | 0 | 0 | 1 | 1 | 1 | 1 | 0 | 0 | 0 | 0 | 1 | 1 | 1 | 1 |
1 | 0 | 0 | 0 | 1 | 1 | 0 | 0 | 1 | 1 | 0 | 0 | 1 | 1 | 0 | 0 | 1 | 1 |
1 | 1 | 0 | 1 | 0 | 1 | 0 | 1 | 0 | 1 | 0 | 1 | 0 | 1 | 0 | 1 | 0 | 1 |
假定F(x,y) G(x,y)分别代表其中两个运算,只要F(x,y)真值为“1”的行上G(x,y)必是“1”,则F(x,y) <= G(x,y)。例如 (x&y )<= x <= x|~y。
使用布尔运算不等式的传递性时要小心,我们知道,如果x+y<=a且z<=x,则z+y<=x+y<=a,但要是加号(+)换成位或( | )则不成立。如 x = 6, y = 2, a =6, z = 5时。
还可以扩展出以下不等式
a ( x | y ) >= max(x,y)
b ( x & y )<=min(x,y)
c ( x | y ) <= x + y 当加法操作不溢出
d ( x | y ) > x + y 当加法操作溢出
e ( x ^ y ) >= | x - y |
算式 a,b,c,d证明都较简单,算式e看似复杂,其实将算式a减算式b就可获证,( x ^ y ) =( x | y ) - ( x & y ) >= max(x,y)-min(x,y) = | x - y | 。
2-4 绝对值函数
要是你的系统上没有求绝对值的指令,那可以试试以下算式,其中,y = x带符号右移31位,即符号位(bit31)是1时,y =~0,是0时y=0。
如果机器没有提供符号右移指令,或是符号右移效率较低,可以无符号右移实现有符号右移,参考2-6
| x | = ( x ^ y ) - y
= ( x + y ) ^ y
= x - ( 2x & y )
第三个算式中2x当然指 x+x或 x<<1。
要是你的系统支持快速乘法运算,也能用下面算式计算绝对值
(( x 符号右移30位)|1) * x
其实,( x 符号右移30位)|1) 在符号位是“1”时结果是 -1(~0),在符号位是0时,结果是1,即将 0,1分别映射为 1,-1,然后和原数值 x 做了乘法运算。
2-5 符号展开
有时需要将8位整数展开为32位整数,现在一般使用类型转换(long)var,也可以用用4逻辑运算可以使用下式。
a ( ( x + 0x00000080 ) & 0x000000FF ) - 0x00000080
b ( ( x & 0x000000FF ) ^ 0x00000080 ) - 0x00000080
第一式中,加号(+)可以换成减号(-)或异或运算符(^),只要在这一步将符号位取反就好。
2-6 用无符号右移实现有符号右移
可以用5-6个指令用无符号右移实现有符号右移实现
a ( ( x + 0x80000000 ) >> n ) - (0x80000000>>n)
其中,加号(+)可以换成减号(-)或异或运算符(^),只要在这一步讲符号位取反就好。31位以上计算结果显然将溢出,不用担心影响右移n位后最左边n位的值。
c t = (x&0x80000000)>>n; (x>>n)-(t+t)
d (x>>n) | ( - (x>>31)<<(31-n)
e t = -(x>>31); (((x^t)>>n)^t
2-7 符号函数
实现下列符号函数,有几种方法,一般都需要4-5条指令。
sign(x) = -1 if x < 0
= 0 if x == 0
=1 if x > 0
如果系统支持有符号右移指令,可用4条指令实现
a ( x 符号右移31位)(-x>>31)
如果没有有符号右移指令,可以按(2-6)中的算式
b -(x>>31)|(-x>>31)
还可以用不等式来实现
c (x>0)-(x<0)
d (x>=0)-(x<=0)
2-8 三值比较函数
cmp(x) = -1 if x < y
= 0 if x == y
= 1 if x > y