位运算技巧(一)

题记

    笔者最近在做密码学方面的项目,有些算法对性能的要求很高,再加上密码学算法经常用到异或操作或者掩码运算,所以对位运算的小技巧一直很关注。在一个偶然的机会发现Stanford一个大牛Sean Eron Anderson自己维护了一个列表,把自己能够找到的所有位运算的算法和小技巧都整理了出来Bit Twiddling Hacks。算法都很短,但是有些算法能够完成的工作和实现的思路着实让人耳目一新。笔者在仔细阅读和分析这些代码的时候,经常像第一次看到Carmack写的快速平方根倒数算法的时候一样,觉得“这tm是什么”,“这tm又是什么”的砸屏幕冲动,到下面一些位数较高的小节时,甚至需要拿笔来算一下为何算法能达到目的。和几个朋友聊了聊,也觉得这些代码的分析不翻译成中文,着实有些可惜了。

    这些算法从最简单的判断int值正负值、交换数据,到比特位翻转等,很丰富。实现目的也基本是减少分支判断、减少数据交换次数等,如果大家对程序基础计算性能要求极高;或者嵌入式开发中内存或者寄存器受局限,必须减少时间或者空间复杂度;或是希望分析一下这些代码的思路;亦或者是单纯想提高一下编程炫技的能力,请尽情选择适合自己的内容取用。

    需要说明的是,有些代码笔者在原有基础上稍微改写了一下,方便调用,但是算法本身并未改动。文字有改写和增删,删去了大部分与主题无关的改动历史记录,大意未变,以方便理解。

    让我们先从Carmack的算法开始,欣赏下这些用位运算实现的匪夷所思的功能。

float Q_rsqrt( float number )
{
        long i;
        float x2, y;
        const float threehalfs = 1.5F;
 
        x2 = number * 0.5F;
        y  = number;
        i  = * ( long * ) &y;                       // evil floating point bit level hacking
        i  = 0x5f3759df - ( i >> 1 );               // what the fuck?
        y  = * ( float * ) &i;
        y  = y * ( threehalfs - ( x2 * y * y ) );   // 1st iteration
//      y  = y * ( threehalfs - ( x2 * y * y ) );   // 2nd iteration, this can be removed
 
        return y;
}

/***********************************************************正式翻译分割线************************************************************/

说明

    本文中的绝大部分算法,如果没有特意指出,都取自公开发表的文章或者开源代码,所以如果你喜欢的话,请尽情地使用它们。这些算法是由Sean Eron Anderson搜集并整理。这些算法被整理出来以期望被使用,但是笔者不保证这些算法在特殊情况下不出现兼容性问题。(但不必担心,)截止2005年5月5号,所有代码都经过了彻底的测试,成千上万的人已经审阅过它们了。值得一提的是,Randal Bryant教授,卡耐基梅隆大学计算机科学学院的院长,亲自在他的 欧几里得系统中进行了绝大部分代码的测试。同时,我也在32位电脑上测试了几乎所有的输入边界条件。

本文关于计算机操作的计算方法

    当统计本文中的算法中的运算次数的时候,任何C运算符被视为1次运算。计算中的赋值,若没有写入RAM中,将不会被计数。当然,这些运算次数的统计方法,只能近似给出实际中的机器命令和CPU时间。有许多细微的差别都可能影响系统运行一个示例的时间快慢,例如缓存大小,内存位宽,指令集等等。因此,考量算法哪些更快的最好方法,是实际测试一下它们,所以尽可能在你的目标机器上运行一下它们试试。

计算一个int值的符号值

首先是最直观的方式:

// v >= 0时,返回0; v < 0时返回-1
int get_sign(int v){
	return -(v < 0);
}

如果想要使用CPU的标志位寄存器来避开分支判断,则使用:
#include <limits.h>
// v > 0时,返回 0; v < 0时返回 -1
int get_sign(int v){
	return -(int)((unsigned int)((int)v) >> (sizeof(int) * CHAR_BIT - 1));
}

上面的方法有最强的通用性,如果以丧失一些可移植性为代价提高性能,则可以使用:
#include <limits.h>
// v > 0时,返回 0; v < 0时返回 -1
int get_sign(int v){
	return v >> (sizeof(int) * CHAR_BIT - 1);
}
最后一种求值表达式中,在32位机器上,sign =  v >>31。这个方法比第一种方法减少一步操作。

若是想让结果呈现v < 0时为 -1,其余为 -1,则可以使用:
#include <limits.h>
// v > 0时,返回 +1; v < 0时返回 -1
int get_sign(int v){
	return +1 | (v >> (sizeof(int) * CHAR_BIT - 1));
}

若更完善一些,希望大于0时返回1,等于0时返回0,小于0时返回-1,则:
#include <limits.h>
// v > 0时,返回+1;v = 0时返回0; v < 0时返回-1
int get_sign(int v){
	return (v != 0) | -(int)((unsigned int)((int)v) >> (sizeof(int) * CHAR_BIT - 1));
}

若以丧失一些可移植性为代价,提高速度,可以使用:
#include <limits.h>
// v > 0时,返回+1;v = 0时返回0; v < 0时返回-1
int get_sign(int v){
	return (v != 0) | (v >> (sizeof(int) * CHAR_BIT - 1));  // -1, 0, or +1
}

还有一种简洁通用,但是速度稍慢的方法
// v > 0时,返回+1;v = 0时返回0; v < 0时返回-1
int get_sign(int v){
	return (v > 0) - (v < 0);
}

若仅仅是想知道一个值是不是非负值,即返回+1或者0,则可以使用
// v >= 0时,返回+1; v < 0时返回0
int isNonNegative(int v){
	return 1 ^ ((unsigned int)v >> (sizeof(int) * CHAR_BIT - 1));
}

检测两个int值是否异号

//检测两个值是否异号
int isDiff(int x, int y){
	return ((x ^ y) < 0);
}
很显然,如果符号位不相同的话,异或值符号位必然为1,值必然为负。

不使用分支结构,计算int值的绝对值

#include <limits.h>
unsigned int abs(int v){
	const int mask = v >> sizeof(int) * CHAR_BIT - 1;
	return (v + mask) ^ mask;
}

    上面这个算法还有另外一个版本,与上面一个版本类似,但是被Sun公司的工程师注册为专利了,慎用:
#include <limits.h>
unsigned int abs(int v){
	const int mask = v >> sizeof(int) * CHAR_BIT - 1;
	return  (v ^ mask) - mask;
}

    有一些CPU并没有计算数值绝对值的硬件结构(或者编译器调用失败),另外,在有些计算机上分支结构的系统开销要大于简单的位运算。在这种情况下,虽然上面的这两种算法与最直观的算法相比操作次数相同,但都可以加速绝对值处理的速度。在这里给出最直观的绝对值算法 :
    absValue = (v < 0) ? -(unsigned)v : v;
    笔者注:值得一提的是,在这两段代码的后面附上了作者关于这个算法的改进记录。作者在这个算法上断断续续改进了12年!记录本身对我们并没有太大价值,所以笔者并未译出,但是这种改进的精神依然值得我们去学习。

不使用分支结构,计算两个int值的最大值和最小值

int min(int x, int y){
	return y ^ ((x ^ y) & -(x < y)); // min(x, y)
}
     在一些设备上,分支结构开销很大,或者CPU没有条件转移指令,那么上面的表达式就比 (x < y) ? x : y计算的要快,虽然前者多了两个操作步骤(但通常情况下直观算法的速度更快:))。这是因为若x < y,那么 -(x < y)输出为全1,那么return值为 y ^ ((x ^ y) & -(x < y)) = y ^ ((x ^ y) & ~0 = y ^ x ^ y = x;若x >= y,那么 -(x < y)为全0,使得返回值为 y ^ ((x ^ y) & 0 = y。在一些设备上,计算 (x < y)的值 0 或1,是需要分支结构的,那么这种方法并不会带来系统开销上的好处。

    由分析,反应足够快的朋友应该已经构造出max算法的形式了:
int max(int x, int y){
	return x ^ ((x ^ y) & -(x < y)); // min(x, y)
}

    给出一个不可移植的快速实现版本(其实无伤大雅,译者注):
如果你可以保证 INT_MIN <= x -y <= INT_MAX,那么可以使用下面这个版本的程序,这个版本速度更快,因为(x - y)运算仅仅需要被计算了一次。
#include <limits.h>
int min(int x, int y){
	return y + ((x - y) & ((x - y) >> (sizeof(int) * CHAR_BIT - 1)));
}
int max(int x, int y){
	return x - ((x - y) & ((x - y) >> (sizeof(int) * CHAR_BIT - 1)));
}
    由于1989 ASCI C 标准并未给出带符号数的右移运算规定,所以在这个版本的编译器上不一定能得出正确值来。如果在计算的过程中发生了溢出的异常,那么x, y的值需要被设置为unsigned类型,或者在做减法的时候转换成unsigned类型以避免溢出,这是因为有符号数的右移运算需要一个符号操作数以得出所有的负值右移中的比特位1。

计算一个int值是否为2的幂

//若是2的幂,则return 1,else return 0
int isPow(int x){
	return x && !(x & (x - 1));
}
这个版本考虑到了若x为0的情况,而最早的版本 return (v & (v - 1)) == 0并未考虑到这个问题。


今天只是热身,代码基本都是一两行即可完成,但是设计依然很精巧,值得一看。明天打算翻译后面的一部分,就会出现所谓的Magic Number 和 Magic Table,那些构造就会更加高效,同时也更加好玩。顺便吐槽一下,CSDN的博客编辑功能不是很好用啊:)



  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值