单变量表示多状态
如果你有使用过SDL2库,你一定见过这样的代码
renderer = SDL_CreateRenderer(window, -1, SDL_RENDERER_ACCELERATED | SDL_RENDERER_PRESENTVSYNC);
这其中有一个奇怪的用法
SDL_RENDERER_ACCELERATED | SDL_RENDERER_PRESENTVSYNC
这里只输入了一个参数,但是就好像是输入了两个
在C++标准库中也有类似的用法
std::fstream fs;
fs.open("a.data",std::ios::in | std::ios::binary);
这是怎么回事呢?我们以std::ios::binary
为例,先来看看它在iso_base.h
中的定义。
// ...
enum _Ios_Openmode
{
_S_app = 1L << 0,
_S_ate = 1L << 1,
_S_bin = 1L << 2,
_S_in = 1L << 3,
_S_out = 1L << 4,
_S_trunc = 1L << 5,
_S_noreplace = 1L << 6,
_S_ios_openmode_end = 1L << 16,
_S_ios_openmode_max = __INT_MAX__,
_S_ios_openmode_min = ~__INT_MAX__
};
// ...
// ...
static const openmode binary = _S_bin;
// ...
可以看出C++标准库的做法是将每一个“标签“用一个64bit(即8byte)整数000...001
按位左移不同的位数得到。使该64bit数据的每一位都表示一个标签的真假,一共可以表示64个不同的标签,再用按位或操作即可将多个标签混合到一个值,具体如下表所示。
值1 | 值2 | 两者按位或 | 结果描述 |
---|---|---|---|
00…001 | 00…010 | 00…011 | 读结果后两位可知,值1,2均表示真 |
00…000 | 00…010 | 00…010 | 读结果后两位可知,值1,2分别表示假,真 |
00…001 | 00…000 | 00…001 | 读结果后两位可知,值1,2分别表示真,假 |
00…000 | 00…000 | 00…000 | 读结果后两位可知,值1,2均表示假 |
可以写一个C程序来验证上述操作。
#include <stdio.h>
// int32_t 自然是32位二进制
// 一个八进制数表示两个比特位
// 一个十六进制数表示四个比特位
// 例如:
// 0x0 = 0000
// 0xF = 1111
// 0xFF = 1111 1111(空格仅为方便阅读,可去除)
// 0xAC = 1010 1100
int check(int input,int val)
{
int mark,i;
for(mark = 0;val != 1;val >>= 1) mark++;
for(i = 0;i < mark;i++) input >>= 1;
return (input & val) == 1;
}
int main(void)
{
int a = 1; //bin: 0000 ... 0000 0001 hex: 0x0...01
int b = 2; //bin: 0000 ... 0000 0010 hex: 0x0...02
int c = 4; //bin: 0000 ... 0000 0100 hex: 0x0...04
int d = 8; //bin: 0000 ... 0000 1000 hex: 0x0...08
int e = 16; //bin: 0000 ... 0001 0000 hex: 0x0...10
int input = a | b | c | d; //bin: 0000 ... 0000 1111 hex: 0x0...0F
printf("%d\n",check(input,a));
printf("%d\n",check(input,b));
printf("%d\n",check(input,c));
printf("%d\n",check(input,d));
printf("%d\n",check(input,e));
return 0;
}
注释中的bin
表示二进制,hex
表示十六进制,此处赋值时直接使用了十进制。
更快的乘2运算
先来看几个二进制数
二进制 | 十进制 |
---|---|
0001 | 1 |
0010 | 2 |
0100 | 4 |
1000 | 8 |
不难看出,亦不难证出一个二进制数按位左移1位即能使其乘以2。
术业有专攻,这种专门乘2的算法理应是要比传统的万用乘法运算符要快的,我们可以通过一个简单的C程序验证这个猜测。
#include <stdio.h>
#include <time.h>
#define PCD 100000
int main(void)
{
int i,j;
long long n = 1;
float s,e;
s = clock();
for(i = 0;i < PCD;i++) for(j = 0;j < PCD;j++) n *= 2;
e = clock();
printf("%lf sec.\n",(e - s)/CLOCKS_PER_SEC);
n = 1;
s = clock();
for(i = 0;i < PCD;i++) for(j = 0;j < PCD;j++) n <<= 1;
e = clock();
printf("%lf sec.\n",(e - s)/CLOCKS_PER_SEC);
}
测试结果如下:
第一次 | 第二次 | 第三次 | 平均 | |
---|---|---|---|---|
n *= 2 | 4.257221 sec | 4.125867 sec | 4. 176448 sec | 4.186512 sec |
n <<= 1 | 3.519814 sec | 3.600015 sec | 3.618779 sec | 3.579536 sec |
确实有所提升,事实上,大多数编译器启用代码优化后都会尝试将n * 2
替换为n << 1
,以此提升程序性能。
更快的除2运算
原理和上述乘2运算一样,测试代码如下:
#include <stdio.h>
#include <time.h>
#define PCD 100000
int main(void)
{
int i,j;
long long n = 10000;
float s,e;
s = clock();
for(i = 0;i < PCD;i++) for(j = 0;j < PCD;j++) n /= 2;
e = clock();
printf("%lf sec.\n",(e - s)/CLOCKS_PER_SEC);
n = 10000;
s = clock();
for(i = 0;i < PCD;i++) for(j = 0;j < PCD;j++) n >>= 1;
e = clock();
printf("%lf sec.\n",(e - s)/CLOCKS_PER_SEC);
}
测试结果如下:
第一次 | 第二次 | 第三次 | 平均 | |
---|---|---|---|---|
n /= 2 | 7.669598 sec | 7.514236 sec | 7.551863 sec | 7.578565 sec |
n >>= 1 | 3.414502 sec | 3.584141 sec | 3.611094 sec | 3.536579 sec |
同样的,大多编译器也会在开启代码优化时尝试将n /2
替换为n >> 1
以提升性能。
哈希函数
哈希函数肯定离不开位运算,以FNV-1a
算法为例:
uint32_t fnv1a_32(char* data)
{
// 32bit version of the algorithm FNV-1a
static const uint32_t HASH_OFFSET_BASIS_32 = 0x811C9DC5;
static const uint32_t FNV_PRIME_32 = 16777619;
uint32_t hash = HASH_OFFSET_BASIS_32;
int len = strlen(data);
for(int i = 0; i < len; i++)
{
hash *= FNV_PRIME_32;
hash ^= data[i];
}
return hash;
}
详见我的另一篇博文。
位运算的一些奇怪用法
更快但有局限的模运算
#define QUICK_MODE(M,N) ((M) & ((N) - 1))
测试代码如下:
#include <stdio.h>
#include <time.h>
#define QUICK_MODE(M,N) ((M) & ((N) - 1))
#define PCD 100000
#define N 2
// 当且仅当 N = 2^n 时 QUICK_MODE(M,N) 可用,否则计算出错
int main(void)
{
float s,e;
int i,j,x1,x2,y = 114514;
s = clock();
for(i = 0;i < PCD;i++) for(j = 0;j < PCD;j++) x1 = y % N;
e = clock();
printf("%lf sec\n",(e-s)/CLOCKS_PER_SEC);
s = clock();
for(i = 0;i < PCD;i++) for(j = 0;j < PCD;j++) x2 = QUICK_MODE(y, N);
e = clock();
printf("%lf sec\n",(e-s)/CLOCKS_PER_SEC);
printf("%s\n",x1 == x2 ? "PASSED":"FAILED");
return 0;
}
输出如下:
5.105534 sec
4.125605 sec
PASSED
更慢但无中间值的交换
void noval_swap(int* m,int* n)
{
*m = *m ^ *n;
*n = *m ^ *n;
*m = *m ^ *n;
}
测试代码:
#include <stdio.h>
#include <time.h>
#define PCD 100000
void traditional_swap(int* m,int* n)
{
int buffer = *m;
*m = *n;
*n = buffer;
}
void noval_swap(int* m,int* n)
{
*m = *m ^ *n;
*n = *m ^ *n;
*m = *m ^ *n;
}
int check(int* m,int* n)
{
int _m = *m, _n = *n;
traditional_swap(m, m);
noval_swap(&_m,&_n);
return (_m == *n && _n == *m);
}
int main(void)
{
float s,e;
int m = 3,n = 2,i,j;
printf("%s\n",check(&m,&n)?"PASSED":"FAILED");
s = clock();
for(i = 0;i < PCD;i++) for(j = 0;j < PCD;j++) traditional_swap(&m, &n);
e = clock();
printf("%lf sec\n",(e-s)/CLOCKS_PER_SEC);
s = clock();
for(i = 0;i < PCD;i++) for(j = 0;j < PCD;j++) noval_swap(&m, &n);
e = clock();
printf("%lf sec\n",(e-s)/CLOCKS_PER_SEC);
return 0;
}
炫酷但更慢的求绝对值
#define ABS(N) ((((N) >> 31) ^ (N)) - ((N) >> 31))
测试代码:
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#define ABS(N) ((((N) >> 31) ^ (N)) - ((N) >> 31))
#define PCD 100000
int abs(int n)
{
return n > 0 ? n : -n;
}
int main(void)
{
int i,j,m,n = -114;
float s,e;
s = clock();
for(i = 0;i < PCD;i++) for(j = 0;j < PCD;j++) m = abs(n);
e = clock();
printf("%lf sec.\n",(e - s)/CLOCKS_PER_SEC);
s = clock();
for(i = 0;i < PCD;i++) for(j = 0;j < PCD;j++) m = abs(n);
e = clock();
printf("%lf sec.\n",(e - s)/CLOCKS_PER_SEC);
s = clock();
for(i = 0;i < PCD;i++) for(j = 0;j < PCD;j++) m = ABS(n);
e = clock();
printf("%lf sec.\n",(e - s)/CLOCKS_PER_SEC);
return 0;
}
输出:
4.285493 sec.
4.521450 sec.
5.298813 sec.