CrazyGo

  博客园  :: 首页  :: 新随笔  :: 联系 :: 订阅 订阅  :: 管理

有趣的位运算(一)基本操作

在c/c++语言中,适当的使用位运算来代替分支判断,可以去除程序中的分支预测,有效的提高程序的执行效率。讲解位运算最著名的要数<<hack's delight>>,

不过网上也有很多牛人总结过位运算心得,位运算无非就是利用数字(通常为整数)的补码表示进行的与,或,非,异或,四大操作,下面是我总结的有趣且常用的一些位运算:

一.最简单的:

1)判断奇偶数:(x & 1) == 0):even,   ((x & 1) == 1) : odd

2)判断低n 个bit位是否为1:

y=x & (1<<n) 

这里通过将正整数1(补码表示为..00001)逻辑左移n位以后,与x进行与运算,来判断第n位的情况,注意,移位计数是从0开始的。即1可以左移0位,1位...n位。

3)则我们可以通过

y=x |(1<<n) 

 来将x的第n个bit位置为1(set).

4)沿着上面的思路,我们可以将1逻辑左移n位后,按位取反,然后再与x相与,即可将x的右属第n位置位0(unset).

y=x &(~(1<<n) )

5)同样呢,我们可以把与运算符变成异或运算,就可以实现将x的右数第n个bit位取反(toggle)的效果,即:

y = x ^ (1<<n)

6) 通常,对于一个整数x,x-1是将x的bit表示的最右侧的10000的形式变成了01111的形式,如:

01010111    (x) -> 01010110    (x-1)
10000000    (x = -128) -> 01111111    (x-1 = 127 (with overflow))
 

 这样,我们就找到了将x的最右侧的bit 1置位0的方法,

y=x&(x-1)

 7)根据6)我们可以得到奖x的最右侧的bit 1的右侧的0全部置位1的方法:

y = x | (x-1)

 8)在补码表示中,对于一个数x, -x=~x+1,

即:

x:10111100
~x:01000011
-x:01000100

 则,我们可以用

y=x&(-x)

 来提取x的bit表示中的最有侧的1,而将其他为都置位0.

9)根据8),我们可以用

y = ~x & (x+1)

 将x的最右侧的0提取出来,置为1,其余的位全部置位0.原理是,~x使x的最右侧的0变为1,其余的比特位一次反转;x+1同样使得最右侧0变为1,这样在,两者相与以后,只会有此位为1,其余位都

变为0.

以上来自这里

二.高阶的

以下的高阶的位运算来自这里,是斯坦福大学的一位教授所整理的,讨论了代码的可移植性以及效率问题,非常nice!

挑一些个人感觉有趣的说明如下:

1)判断一个数的正负:

const int  CHAR_BIT =8;//此处为了易于说明,为本人加入
 int v;      // we want to find the sign of v
int sign;   // the result goes here 

// CHAR_BIT is the number of bits per byte (normally 8).
sign = -(v < 0);  // if v < 0 then -1, else 0. 
// or, to avoid branching on CPUs with flag registers (IA32):
sign = -(int)((unsigned int)((int)v) >> (sizeof(int) * CHAR_BIT - 1));
// or, for one less instruction (but not portable):
sign = v >> (sizeof(int) * CHAR_BIT - 1); 

上面代码给出了3中方法来查看整数v的符号,第一个sign = -(v < 0),即看v是否小于0,为真则sign=-1,反之则为0,含有小于号,在有的平台上可能会出现条件分支,故紧接着给出了去除分支的方法:

a: sign = -(int)((unsigned int)((int)v) >> (sizeof(int) * CHAR_BIT - 1))和

b:sign = v >> (sizeof(int) * CHAR_BIT - 1);

采用的思想是算术右移(sizeof(int) * CHAR_BIT - 1)个整数位,使得符号位跑到最右侧,若v为负数,则sign=-1,否则位0。

这里作者又说了,a式代码是可一直的,b式的代码是不可移植的,a式进行了强制类型转换,但具体原因是什么,目前本人还没弄懂(感觉是针对老板的c语言说的,即1989 ANSI C)。

当然,根据上式,我们可以把结果写的更明确些,如:

sign = +1 | (v >> (sizeof(int) * CHAR_BIT - 1));  // if v < 0 then -1, else +1

返回值为-1或1.
或者:

sign = (v != 0) | -(int)((unsigned int)((int)v) >> (sizeof(int) * CHAR_BIT - 1));
// Or, for more speed but less portability:
sign = (v != 0) | (v >> (sizeof(int) * CHAR_BIT - 1));  // -1, 0, or +1

返回值为-1,0,1.

如果小于号可以被编译器优化的话,则还有:

sign = (v > 0) - (v < 0); // -1, 0, or +1

最后,当我们判断v是否是非负数时,即v>0,返回1,其余返回0,则这里异或运算就派上用场了:

sign = 1 ^ ((unsigned int)v >> (sizeof(int) * CHAR_BIT - 1)); // if v < 0 then 0, else 1

 

2)判断两个整数是否异号:

int x, y;               // input values to compare signs

bool f = ((x ^ y) < 0); // true iff x and y have opposite signs

当然,跟1)式,我们也可以去除比较符号:

int x, y;               // input values to compare signs

int f = ((x ^ y) >> (sizeof(int) * CHAR_BIT - 1)));  // true iff x and y have opposite signs

 

3)计算整数的绝对值:

int v;           // we want to find the absolute value of v
unsigned int r;  // the result goes here 
int const mask = v >> sizeof(int) * CHAR_BIT - 1;

r = (v + mask) ^ mask;

这里如果v位整数,则mask值为0,r=v;如果v位负数,mask为全1,v+mask使得v最右侧的1及其此位右侧的0全部取反,这详单与对原数v按位取反,即,(+v+mask)=~(+v),然后呢,~(+v)与掩码mask相异或,即使将v的各比特位按位取反,最后得到+v.

另一个可选的方法是:

r = (v ^ mask) - mask;

4)计算两个整数中的最大最小值:

int x;  // we want to find the minimum of x and y
int y;   
int r;  // the result goes here 

r = y ^ ((x ^ y) & -(x < y)); // min(x, y)
r = x ^ ((x ^ y) & -(x < y)); // max(x, y)

对于min(x,y),当x<y时,-(x<y)为-1,即11..111,这样就变成了,r=y ^ (x ^ y)=x, max(x,y)的计算方法类似。
在一些没有条件转移指令的机器上,可以采用以上方法来计算最大最小值,如果有条件转移指令,则可以:

r=x>y?y:x;//min(x,y)

这样也可以去除分支。可以参考体系结构类的书籍,如《深入理解计算机系统》,或这里.

5)判断一个数是否为2的n次幂:

unsigned int v; // we want to see if v is a power of 2
bool f;         // the result goes here 

f = (v & (v - 1)) == 0;

当我们要考虑v是否为0时(0不是2的幂),可以得到:

f = v && !(v & (v - 1));

6)计算一个数含有多少个比特1:

unsigned int v; // count the number of bits set in v
unsigned int c; // c accumulates the total bits set in v
for (c = 0; v; c++)
{
  v &= v - 1; // clear the least significant bit set
}

此题在<<编程之美>>中叶出现过,还有其他方法,如查表法等。

7)反转一个数的比特位,即1110001变成1000111。

unsigned int v;     // input bits to be reversed
unsigned int r = v; // r will be reversed bits of v; first get LSB of v
int s = sizeof(v) * CHAR_BIT - 1; // extra shift needed at end

for (v >>= 1; v; v >>= 1)
{   
  r <<= 1;
  r |= v & 1;
  s--;
}
r <<= s; // shift when v's highest bits are zero

思路是,每次取得v最右侧的一个比特位以后,放在r的最右侧,然后v右移,r左移。最后考虑到,当v位整数时,后面会出现一串的0,导致循环结束,这样,就要将r在左移相应的位数,即s位。当然还有很多方法
来实现位的反转,这是最简单易于理解的一种。

另外还有:

unsigned int v; // 32-bit word to reverse bit order

// swap odd and even bits
v = ((v >> 1) & 0x55555555) | ((v & 0x55555555) << 1);
// swap consecutive pairs
v = ((v >> 2) & 0x33333333) | ((v & 0x33333333) << 2);
// swap nibbles ... 
v = ((v >> 4) & 0x0F0F0F0F) | ((v & 0x0F0F0F0F) << 4);
// swap bytes
v = ((v >> 8) & 0x00FF00FF) | ((v & 0x00FF00FF) << 8);
// swap 2-byte long pairs
v = ( v >> 16             ) | ( v               << 16);

以及

unsigned int s = sizeof(v) * CHAR_BIT; // bit size; must be power of 2 
unsigned int mask = ~0;         
while ((s >>= 1) > 0) 
{
  mask ^= (mask << s);
  v = ((v >> s) & mask) | ((v << s) & ~mask);
}


以上参考自这里

 

posted on 2013-06-30 19:55  CrazyGo  阅读(552)  评论(0)    收藏  举报