位运算详解


前言

本文将从基础位运算出发,逐步带大家感受到真实工程中位运算的应用和魅力。

一、与、或、非、异或

或运算

或运算常用于设置数据中某一bit位,即该位置1。如果要设置 a a a 的第 n b i t n bit nbit 位(从0开始)那么有公式如下:a |= (1<<n)

应用如下所示:

a10000000
a|=(1<<1)a=10000010
a|=(1<<3)a=10001000
a|=(1<<5)a=10100000

与运算和非运算

1:与运算搭配非运算常用于清除数据中某一bit位,即该位置0。如果要清除 a a a 的第 n b i t n bit nbit 位(从0开始)那么有公式如下:a &= ~(1<<n)
2:与运算常用于测试数据中某一bit位。如果要测试 a a a 的第 n b i t n bit nbit 位(从0开始)那么有公式如下:a &= (1<<n)。主要观察结果是否非0,非0则该位为1,否则为0。

清除 bit应用如下所示:

a11111111
a &= ~(1<<1)a=11111101
a &= ~(1<<3)a=11110111
a &= ~(1<<5)a=11011111

测试bit应用如下,重点观察结果为0与否与该位是否为1的关系:

a00101010
a &= (1<<1)a=00000010
a &= (1<<3)a=00001000
a &= (1<<6)a=00000000

异或运算

异或运算常用于将数据中某一bit位反转,即该位为1则置0,该位为0则置1。如果要反转 a a a 的第 n b i t n bit nbit 位(从0开始)那么有公式如下:a ^= (1<<n)
反转bit应用如下:

a11110000
a ^= (1<<1)a=11110010
a ^= (1<<3)a=11111000
a ^= (1<<6)a=10110000

同时操作多位

根据各运算的特点,我们可根据实际需要同时设置清除反转多位。
同时清除最后3bit位如下:

a11111111
a &= ~((1<<0) | (1<<1) | (1<<2)) <==> a &= ~7a=11111000

同时设置最后4bit位如下:

a11110000
a |= ((1<<0) | (1<<1) | (1<<2) | (1<<3))<==> a |= 15a=11111111

同时反转最后4bit位如下:

a11110000
a ^= ((1<<0) | (1<<1) | (1<<2) | (1<<3))<==> a ^= 15a=11111111

二、位运算简单应用枚举

x & (x-1)判断数据x是否为2的整数次幂

Linux内核5.10.203中有判断是否为2的整数次幂函数

bool is_power_of_2(unsigned long n)
{
	return (n != 0 && ((n & (n - 1)) == 0));
}

我们此时以8位数据 x = 01011000 x=01011000 x=01011000 为例说明原理。

x01011000
x-101010111
x & (x-1)01010000

比较第一行 x x x 和最后一行 x & ( x − 1 ) x\&(x-1) x&(x1),我们得到以下变化:
1: x & ( x − 1 ) x\&(x-1) x&(x1) 对比 x x x 所有最右1bit右侧的0bit位仍旧为0。上述表格运算中,第3位右侧的第 012 0 1 2 012位的0bit不变。
2: x & ( x − 1 ) x\&(x-1) x&(x1) 对比 x x x 最右的一个1bit位变为0,由于 x − 1 x-1 x1 需要借位的缘故。上述表格运算中,第3位变0。
3: x & ( x − 1 ) x\&(x-1) x&(x1) 对比 x x x 其余bit位不变。

那么当 x & ( x − 1 ) x\&(x-1) x&(x1) 为 0 时,数据只有一个1bit位,且该bit位是位于该数据二进制表示中最右的1bit位。显然,该数据是2的整数次幂。

strlen优化

如今计算机字长多为32或64,按照下图中的传统strlen函数中一字节一判断是否为空字符在执行效率上已经太慢太慢。

unsigned int strlen(const char *s) {
    char *p = s;
    while (*p != ’\0) p++;
    return (p - s);
}

FreeBSD最新strlen函数源码 中对strlen函数在执行效率上对源码做了修正。当计算机字长为64时,简化整理关键部分源码如下所示,读者若有兴趣可详细研究全部源码:

#define testbyte(x)				\
	do {					\
		if (p[x] == '\0')		\
		    return (p - str + x);	\
	} while (0)
	
#define	LONGPTR_MASK (sizeof(long) - 1)
	
bool haszero (unsigned long x)
{
  return (x - 0x0101010101010101) & (~x &  0x8080808080808080);
}

size_t strlen(const char *str)
{
	const char *p;
	const unsigned long *lp;
	long va, vb;

  // 控制地址边界对齐,方便按字操作,下面会详细解释
  lp = (const unsigned long *)((uintptr_t)str & ~LONGPTR_MASK);

	
	if (haszero(*lp)){
	  lp++;
		/* 从指针开始位置开始扫描 */
		for (p = str; p < (const char *)lp; p++)
			if (*p == '\0')
				return (p - str);
	}		
	for (; ; lp++) {
		if (haszero(*lp)) {
			p = (const char *)(lp);
			testbyte(0);
			testbyte(1);
			testbyte(2);
			testbyte(3);
			testbyte(4);
			testbyte(5);
			testbyte(6);
			testbyte(7);
		}
	}

	/* 不会到此 */
	return (0);
}

将算法简化为8位一字节来重写算法核心有如下三步:
1:a = (x - 0x01)
2:b = (~ x & 0x80)
3:计算 a & b == 0。若结果非0,则该字节为0。
检测0算法解析:
第一步:如果字节值小于等于 0 x 80 0x80 0x80 ,那么减一之后的结果必然小于 0 x 80 0x80 0x80 ,只有0除外。对于单字节而言, 0 − 1 = 0 x f f 0-1=0xff 01=0xff。也就是说:对于单字节而言,要使得减法结果大于等于 0 x 80 0x80 0x80,有以下限制 x = 0 x=0 x=0 x > 0 x 80 x>0x80 x>0x80
第二步:如果我们限制字符串中所有字符都只为 A S C I I ASCII ASCII 码表中的字符,最大 0 x 7 f 0x7f 0x7f 表示键盘上的 D E L DEL DEL,那么对于32位字长我们可以直接通过 ( x − 0 x 01010101 ) & 0 x 80808080 (x-0x01010101) \& 0x80808080 (x0x01010101)&0x80808080 来直接操作机器字判断0的存在。
第三步:考虑到字节值会大于等于 0 x 81 0x81 0x81 ,我们计算~x & 0x80。对于单一字节值 x x x,当且仅当 x < 0 x 80 x<0x80 x<0x80时,此时~x & 0x80结果为 0 x 80 0x80 0x80
最后:综上所述,当两步结果相与,结果不为0,那么该字节值 x x x同时满足两个条件:
1: x = 0 x=0 x=0 x > 0 x 80 x>0x80 x>0x80
2: x < 0 x 80 x<0x80 x<0x80
那么该字节值只能为0。 扩展到32位,64位等等皆适用。优点在于:前两步可以并发执行,互不影响,最后一步在寄存器之间进行。

对齐首字处理解析:
上面源码中还有非常重要的一步:先处理第一个字。
单独处理的原因在于地址可能不对齐。一般情况下,内存分配器会以字边界开始分配内存,因此多数情况下是对齐的。然而,即使不对齐,那么指针倒退到上一个整字对齐位置(例如:字长为8,指针为0x9,则退到0x8位置对齐),指针和它的上一个整字对齐位置必然位于内存同一页之上,访问不会越界,不会造成缺页中断等奇怪错误。此时如果检测到字中有字节为0,那么我们从指针开始位置扫描到字结束,没有0则字符串没有结束。
另外,最终的字符串结束符位于有效内存页中,那么其所在的对齐字整字必然位于同一有效内存页之上。即,也不会发生越界。

计算对数 l o g 2 N log_2N log2N l o g 10 N log_{10}N log10N

一个较为直观的计算 l o g 2 N log_2N log2N的函数如下所示:
要求参数 N N N必须大于0

int log2(unsigned int N){
  int BITS = 31;
  while (BITS) {
      if (N & 0x80000000) break;
      N <<= 1;
      BITS--;
  }
  return BITS;
}

此处结果正等于 31 31 31减去目标数字 N N N二进制下最高位到最高有效的1bit位中连续的0的个数。

因为 32-bit unsigned integer 最大只能表示 4294967295 U 4294967295U 4294967295U,所以 32 − b i t 32-bit 32bit l o g 10 log_{10} log10 的值只有可能是 0 − 9 0 - 9 09。通过查表法,以省去除法的成本。

计算 l o g 10 N log_{10}N log10N的函数如下所示:
要求参数 N N N必须大于0

int log10(unsigned int N){
  int result=0;
  unsigned int vals[] = {
    1UL,
    10UL,
    100UL,
    1000UL,
    10000UL,
    100000UL,
    1000000UL,
    10000000UL,
    100000000UL,
    1000000000UL,
  };
  for (;result<10;++result){
    if(N >= vals[result] && N <vals[result+1])break;
  }
  return result;
}

知识扩展

Intel® Streaming SIMD Extensions S S E 4.2 SSE4.2 SSE4.2 引入指令中有四条指令 ( P c m p E s t r I , P c m p E s t r M , P c m p I s t r I , P c m p I s t r M ) (PcmpEstrI, PcmpEstrM, PcmpIstrI, PcmpIstrM) (PcmpEstrI,PcmpEstrM,PcmpIstrI,PcmpIstrM)可被用于加速包括 s t r c m p , m e m c m p , s t r l e n , s t r s t r strcmp,memcmp,strlen,strstr strcmpmemcmpstrlenstrstr等的文本处理操作。
P c m p I s t r I PcmpIstrI PcmpIstrI 指令为例,用于对具有隐式长度的字符串数据执行打包比较,生成索引,并将结果存储在ECX中。
指令格式:PCMPISTRI xmm1, xmm2/m128, imm8
三个操作数,第一个操作数为 x m m xmm xmm 寄存器,第二个操作数为 x m m xmm xmm寄存器或指向128bit字符串,最后一个为 8 b i t 8bit 8bit 控制字,控制指令实际比较行为。

当第三个控制字值为二进制序列 1000 b 1000b 1000b 时,会逐个比较前两个操作数中的每一个字符。PcmpIstrI xmm0, dqword[edx + eax], 1000b指令效果为当寄存器edx+eax所指向地址开始的128bit=16byte中如果有某一个byte0,则设置ecx为该byte16byte中的下标(从1开始),否则设置ecx为16。具体见 Intel官方文档

综上,有如下修改版strlen

strlen_sse42:
  ; ecx = string
  mov eax, -16
  mov edx, ecx
  pxor xmm0, xmm0

STRLEN_LOOP:
    add eax, 16
    PcmpIstrI xmm0, dqword[edx + eax], EQUAL_EACH
    jnz STRLEN_LOOP

  add eax, ecx
  ret

此版本strlen函数合理利用硬件加速,单次可处理 16 b y t e 16byte 16byte 数据。速度更快,效率更高。

最后

若文档失效等问题可提醒重新补链接。希望大家多多指正,必定虚心受教。

  • 25
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值