随笔 - 90  文章 - 0 评论 - 87 trackbacks - 0

什么是信息?在计算机世界,信息就是位的序列。这么说它很重要咯?那我们就来讨论一些实用的位运算技巧吧。

(*) 危险的unsigned
在C/C++里有unsigned char/short/int/long。它和默认的signed的不同在于头一位不当作符号,而当作数值的一部分。
尽量避免unsigned在加减乘除和比较运算中出现,否则可能产生意想不到的错误。例如:
unsigned u = -1;
int i = 1;
if(i > u) // 按说1>-1是不争的事实,但结果并非如此
原因是signed int和unsigned int做比较的时候,会把signed int提升成unsigned int.所以实际上是两个无符号整数0x00000001和0xffffffff在比较。
通常只在位运算中才使用unsigned,原因是unsigned类型不会进行符号扩展。下面一段C++代码很好的展示了这一点。
char ch = 128;
unsigned 
char uch = 128;
short ui;
// 整数提升时的符号位扩展            
ui = ch;
cout 
<< hex << ui << endl; // 输出: ff80
ui = uch;
cout 
<< ui << endl; // 输出: 80
cout << short(ch) << endl; // 输出: ff80
cout << short(uch) << endl; // 输出: 80
// 右移时的符号位扩展
cout << (ch >> 1<< endl; // 输出: ffffffc0
cout << (uch >> 1<< endl; // 输出: 40
// 注意,在进行位运算后,会把字符提升为整数,可用下面代码进行验证:    
cout << typeid(ch >> 1).name() << endl; // 输出: int
cout << typeid(ch | 0).name() << endl; // 输出: int


(*) 得到形如00001111 和 11110000的掩码
 // 形如11110(n)
mask = -1 << n;


// 形如00001(n)
mask = ~(-1 << n);

// 形如0(n)1111
mask = -1u >> n;
或者
mask = -1 << ((sizeof mask * 8) - n);  (sizeof mask * 8 是先算出mask所占的字节数,再乘8得到总位数)

// 形如1(n)0000
mask = ~(-1u >> n);
或者
mask = -1 << ((sizeof mask * 8) - n);
虽然在代码写法上第一种方法比第二种方法简单,但它可能报警告unary minus operator applied to unsigned type, result still unsigned。而且第二种方法由于在编译时就已经计算出了(sizeof mask * 8)的值,所以运行效率不比第一种差。


(*) 提高mod计数器效率
让seq在0...N之间循环递增,一般会想到用mod运算:
seq = (seq + 1) % (N + 1);
但mod运算消耗较高,不如以下的方法:
if(++seq > N)
 seq = 0;

如果N是2的幂减1(二进制如00011111),可以用下面更快的方法
seq = ++seq & N;
再强调一遍,这种算法只有在N是2的幂减1时才能用。而事实上,为了充分利用整数中的每一个位,往往都把序号的上限设计成这种形式,如0xefffffff。


(*) 不用临时变量的swap

这是一道常考的面试题。有两种解法最为常见:

= x + y;
= x - y; // x + y - y == x + (y - y) == x + 0 == x
= x - y; // x + y - x == y + (x - x) == y + 0 == y

= x ^ y;
= x ^ y; // x ^ y ^ y == x ^ (y ^ y) == x ^ 0 == x
= x ^ y; // x ^ y ^ x == y ^ (x ^ x) == y ^ 0 == y

第一种不难理解,而第二种用异或操作就比较令人费解了。原因是大部分人不知道异或的逆运算正是异或本身。只要掌握了“逆运算”的要领,这道题还可以有其他很多种解法,例如:

= x * y;
= x / y; // x * y / y == x
= x / y; // x * y / x == y

在所有方法中异或法是最好的,因为用加减或乘除可能导致溢出。
但事实上,以上所有这些swap的方法都是不保险的。例如当x和y是同一个变量的引用:

int a = 1;    
int& x = a;
int& y = a;    
= x ^ y; // x = 1 ^ 1 == 0
= x ^ y; // y = 0 ^ 0 == 0
= x ^ y; // x = 0 ^ 0 == 0

因此,在实际编码中不要耍花招,还是用临时变量吧。



(*) 位的置1与清0
学会了使用mask,位运算就算入门了。
置1: |=
清0: &=
判断某一位是否为1: &
例如:

#define BIT_SET(integer, offset)        ((integer) |= 1 << (offset))
#define BIT_CLR(integer, offset)        ((integer) &= ~(1 << (offset)))
#define BlT_ISSET(integer, offset)      ((integer) & (1 << (offset)))

但只有整型以及可看作整型的变量才能进行位运算,如果要把一块内存中的某一位置1或清0,就要费些脑筋了。
思路是:先定位到一个字节,再对该字节进行位运算。
例如,buf是一个char型指针,我们要把以buf为首地址,偏移量为offset的位置1,则:
buf[offset / 8] |= 1 << (offset % 8);
将这行代码进行效率优化,得到:
buf[offset >> 3] |= 1 << (offset & 7);
于是,最强悍的位置1和位清0函数诞生了:

void    bit_set(void *buf, int offset)
{
    ((
char*)buf)[offset >> 3|= 1 << (offset & 7);
}

void    bit_clr(void *buf, int offset)
{
    ((
char*)buf)[offset >> 3&= ~(1 << (offset & 7));
}

int    bit_isset(void *buf, int offset)
{
    
return ((char*)buf)[offset >> 3& 1 << (offset & 7);
}

要进一步提高效率,还可以把上面三个函数改写成inline或宏,只是在用宏的时候稍加注意就行。


(*) 内存对齐
在自制的内存管理器中,经常需要内存对齐,否则会有总线错误。举例来说,一个int型变量占4字节,则它在内存中的地址必须是4的倍数。
那么如何把一块内存对齐呢?换句话说,如何把一个整数(变量地址)圆整为某个数(其类型所占的字节数)的倍数呢?下面是使用了位运算的一种方法:

#include <iostream>
using namespace std;

#define _ALIGN(addr, T)        (   (unsigned)( (char*)addr + sizeof(T) - 1 ) & ~(sizeof(T) - 1)   )
int main()
{
    
char c;
    cout 
<< hex << (unsigned)&<< ' ' << _ALIGN(&c, double<< endl;    
}

解释一下宏_ALIGN(addr, T)
其中addr是要进行对齐的地址,T是按什么类型对齐的那个类型。
用(unsigned)把一个char*强制转换成整数,因为编译器不允许直接对一个char *进行位运算。
用(char*)强制转换成pointer to char,这样才能与后面加上的指针偏移量sizeof(T) - 1相吻合。
~00000111(b) == 11111000(b),这是一个掩码,为的是把它变成某个数(必须是2的N次幂)的倍数。而这个按位与其实是做减法,但我们只能向后对齐,所以要保证对齐后的内存不能比原来小,这就是+ sizeof(T) - 1的原因。


(*) 不要滥用移位来代替乘除
很多人学了位运算以后,就总想耍一些小把戏,比如乘除2的次幂都用移位来做。
但大部分人没有注意到,对于有符号整数,右移运算和除法不能等价。请看下例:
cout << "-1 >> 1 = " << (-1 >> 1)  << endl;
cout 
<< "-1 / 2 = " << (-1 / 2)  << endl;
cout 
<< "-3 >> 1 = " << (-3 >> 1)  << endl;
cout 
<< "-3 / 2 = " << (-3 /2)  << endl;
cout 
<< "-2 >> 1 = " << (-2 >> 1)  << endl;
cout 
<< "-2 / 2 = " << (-2 / 2)  << endl;
cout 
<< "-16 >> 1 = " << (-16 >> 1)  << endl;
cout 
<< "-16 / 2 = " << (-16 / 2)  << endl;
程序的输出结果:
-1 >> 1 = -1
-1 / 2 = 0
-3 >> 1 = -2
-3 / 2 = -1
-2 >> 1 = -1
-2 / 2 = -1
-16 >> 1 = -8
-16 / 2 = -8
我们发现,负奇数的运算有偏差,负偶数则没有问题。这是因为对整数的除法,当不能整除时采取“向原点取整”(在x86用VS2008编译),负奇数因为无法被2整除所以出现了偏差。如果采取向数轴负方向取整的策略,就不存在任何偏差了,但怎么取整不是我们能说了算的:)
向原点取整自有它的道理,只有向原点取整才能让下面等式成立:
-(1/2) == -1/2
为了我们的计算机不至于变成数学白痴,还是采用向原点取整吧:)
所以对有符号数,要尽量避免位运算。对无符号的类型,则可以放心大胆地使用。
posted on 2008-06-09 21:45 MainTao 阅读(...) 评论(...) 编辑 收藏