文章目录
前言
本文将从基础位运算出发,逐步带大家感受到真实工程中位运算的应用和魅力。
一、与、或、非、异或
或运算
或运算常用于设置数据中某一bit位,即该位置1。如果要设置
a
a
a 的第
n
b
i
t
n bit
nbit 位(从0开始)那么有公式如下:a |= (1<<n)
应用如下所示:
a | 10000000 |
---|---|
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
应用如下所示:
a | 11111111 |
---|---|
a &= ~(1<<1) | a=11111101 |
a &= ~(1<<3) | a=11110111 |
a &= ~(1<<5) | a=11011111 |
测试bit
应用如下,重点观察结果为0与否与该位是否为1的关系:
a | 00101010 |
---|---|
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
应用如下:
a | 11110000 |
---|---|
a ^= (1<<1) | a=11110010 |
a ^= (1<<3) | a=11111000 |
a ^= (1<<6) | a=10110000 |
同时操作多位
根据各运算的特点,我们可根据实际需要同时设置清除反转多位。
同时清除最后3bit位如下:
a | 11111111 |
---|---|
a &= ~((1<<0) | (1<<1) | (1<<2)) <==> a &= ~7 | a=11111000 |
同时设置最后4bit位如下:
a | 11110000 |
---|---|
a |= ((1<<0) | (1<<1) | (1<<2) | (1<<3))<==> a |= 15 | a=11111111 |
同时反转最后4bit位如下:
a | 11110000 |
---|---|
a ^= ((1<<0) | (1<<1) | (1<<2) | (1<<3))<==> a ^= 15 | a=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 为例说明原理。
x | 01011000 |
---|---|
x-1 | 01010111 |
x & (x-1) | 01010000 |
比较第一行
x
x
x 和最后一行
x
&
(
x
−
1
)
x\&(x-1)
x&(x−1),我们得到以下变化:
1:
x
&
(
x
−
1
)
x\&(x-1)
x&(x−1) 对比
x
x
x 所有最右1bit
右侧的0bit
位仍旧为0。上述表格运算中,第3位右侧的第
012
0 1 2
012位的0bit
不变。
2:
x
&
(
x
−
1
)
x\&(x-1)
x&(x−1) 对比
x
x
x 最右的一个1bit
位变为0,由于
x
−
1
x-1
x−1 需要借位的缘故。上述表格运算中,第3位变0。
3:
x
&
(
x
−
1
)
x\&(x-1)
x&(x−1) 对比
x
x
x 其余bit
位不变。
那么当
x
&
(
x
−
1
)
x\&(x-1)
x&(x−1) 为 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
0−1=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
(x−0x01010101)&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
32−bit
l
o
g
10
log_{10}
log10 的值只有可能是
0
−
9
0 - 9
0−9。通过查表法,以省去除法的成本。
计算 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
strcmp,memcmp,strlen,strstr等的文本处理操作。
以
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
中如果有某一个byte
为0
,则设置ecx
为该byte
在16byte
中的下标(从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 数据。速度更快,效率更高。
最后
若文档失效等问题可提醒重新补链接。希望大家多多指正,必定虚心受教。