题记
笔者最近在做密码学方面的项目,有些算法对性能的要求很高,再加上密码学算法经常用到异或操作或者掩码运算,所以对位运算的小技巧一直很关注。在一个偶然的机会发现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;
}
/***********************************************************正式翻译分割线************************************************************/
说明
本文关于计算机操作的计算方法
计算一个int值的符号值
首先是最直观的方式:
// v >= 0时,返回0; v < 0时返回-1
int get_sign(int v){
return -(v < 0);
}
#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。这个方法比第一种方法减少一步操作。
#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;
}
#include <limits.h>
unsigned int abs(int v){
const int mask = v >> sizeof(int) * CHAR_BIT - 1;
return (v ^ mask) - mask;
}
笔者注:值得一提的是,在这两段代码的后面附上了作者关于这个算法的改进记录。作者在这个算法上断断续续改进了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,是需要分支结构的,那么这种方法并不会带来系统开销上的好处。
int max(int x, int y){
return x ^ ((x ^ y) & -(x < y)); // min(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的博客编辑功能不是很好用啊:)