8、位运算技巧:用位运算替代乘除法、判断奇偶、交换变量、位掩码操作
位运算这东西,很多初学者觉得它晦涩,甚至觉得“现在CPU这么快,何必自找麻烦?”
嗯,我年轻时也这么想过。直到我在一个嵌入式项目中,遇到一个每秒要处理几万次中断的传感器驱动——那时候我才真正明白,位运算不是炫技,是刚需。
说白了,位运算就是直接操作二进制位。加减乘除在底层也是靠位运算实现的,那我们为什么不直接一点?
核心思想: 位运算比算术运算快一个数量级,且不依赖CPU的乘法器/除法器。在资源受限的MCU上,这往往是性能瓶颈的突破口。
8.1 用位运算替代乘除法
先看最常见的:乘以2的幂、除以2的幂。
左移一位相当于乘以2,右移一位相当于除以2(注意:仅对无符号整数或正数有效)。
// 传统写法
int x = 10;
int y = x * 8; // 乘以8
int z = x / 4; // 除以4
// 位运算写法
int y = x << 3; // 左移3位,相当于乘以8
int z = x >> 2; // 右移2位,相当于除以4
我个人习惯在循环计数、数组索引计算时,优先用移位。比如一个像素缓冲区,每个像素占4字节,索引计算写成 pixel_offset = index << 2,比 index * 4 快一截。
避坑指南: 我曾经在ARM Cortex-M0上写过一个音频算法,用右移代替除法处理有符号数,结果负数全算错了。记住:有符号整数的右移是算术右移(补符号位),不是逻辑右移。处理负数时,老老实实用除法,或者先转成无符号再移位。
| 操作 | 算术运算 | 位运算替代 | 适用场景 |
|---|---|---|---|
| 乘以2 | x * 2 | x << 1 | 无符号/正数 |
| 除以2 | x / 2 | x >> 1 | 无符号/正数 |
| 乘以2^n | x * (1 << n) | x << n | 无符号/正数 |
| 除以2^n | x / (1 << n) | x >> n | 无符号/正数 |
8.2 判断奇偶:比取模快多了
判断一个数是奇数还是偶数,新手喜欢写 if (x % 2 == 0)。但取模运算其实挺慢的——它本质上是除法。
用位运算:看最低位是0还是1。
// 传统写法
if (x % 2 == 0) {
// 偶数
}
// 位运算写法
if ((x & 1) == 0) {
// 偶数
}
为什么?因为任何偶数的二进制最低位都是0,奇数都是1。你想想看,x & 1 只检查一个bit,而 x % 2 要跑一遍除法器。在循环里频繁调用时,差距就出来了。
我在一个LED闪烁控制程序里,用 (counter & 0x3F) == 0 代替 counter % 64 == 0,中断服务函数的执行时间直接砍掉了一半。
8.3 交换变量:不用临时变量
交换两个变量的值,教科书教我们用临时变量。但位运算提供了一种“炫技”方式——异或交换。
// 传统写法
int tmp = a;
a = b;
b = tmp;
// 位运算写法(异或交换)
a = a ^ b;
b = a ^ b;
a = a ^ b;
原理很简单:x ^ x = 0,x ^ 0 = x。三步异或,就把值换过来了。
注意: 异或交换有个坑——如果 a 和 b 指向同一个内存地址(比如 swap(&x, &x)),结果会把变量清零。我曾在代码审查时见过这种bug,排查了半天才发现是自交换导致的。所以,我建议只在明确知道两个变量不同时才用,否则老老实实用临时变量。
8.4 位掩码操作:寄存器的灵魂
嵌入式开发中,寄存器操作是家常便饭。一个32位寄存器,可能同时控制GPIO方向、上下拉、复用功能。你不能直接读写整个寄存器,否则会破坏其他位。
这时候就需要位掩码:用 & 清零,用 | 置位,用 ^ 翻转。
// 假设寄存器地址 0x40020C00,控制GPIOA的端口输出
#define GPIOA_ODR (*(volatile uint32_t *)0x40020C00)
// 只将第5位置1(其他位不变)
GPIOA_ODR |= (1 << 5);
// 只将第5位清零
GPIOA_ODR &= ~(1 << 5);
// 翻转第5位
GPIOA_ODR ^= (1 << 5);
// 同时操作多个位:将bit3-bit5设置为101
GPIOA_ODR = (GPIOA_ODR & ~(0x7 << 3)) | (0x5 << 3);
这里有个小技巧:用宏定义把位操作封装起来,代码可读性会好很多。
#define SET_BIT(reg, bit) ((reg) |= (1 << (bit)))
#define CLR_BIT(reg, bit) ((reg) &= ~(1 << (bit)))
#define GET_BIT(reg, bit) (((reg) >> (bit)) & 1)
#define TOGGLE_BIT(reg, bit) ((reg) ^= (1 << (bit)))
// 使用
SET_BIT(GPIOA_ODR, 5); // 点亮LED
CLR_BIT(GPIOA_ODR, 5); // 熄灭LED
我在一个电机驱动项目中,需要同时更新6个PWM通道的占空比。如果用结构体+赋值,每次更新要几十条指令。换成位掩码直接操作寄存器,整个更新函数压缩到5条指令以内——中断延迟从3μs降到了0.5μs。
8.5 知识体系总览
下面这张图,把本章的核心知识点串起来了。你可以把它当作一个速查地图。
8.6 总结一下
位运算不是什么高深莫测的东西,它就是用CPU最擅长的方式跟硬件对话。
- 乘除2的幂 → 用移位
- 判断奇偶 → 用 & 1
- 交换变量 → 用异或(注意避坑)
- 寄存器操作 → 用位掩码
你想想看,这些技巧加起来,可能让你的关键代码提速2~5倍,而且不花一分钱硬件成本。这种优化,才是真正的“性价比之王”。
我的建议: 平时写代码时,养成“先想位运算”的习惯。哪怕最后没用上,这个思考过程也会让你对数据在内存中的表示越来越敏感。久而久之,你看到数字,脑子里浮现的是二进制——那时候,你就真的入门了。
公众号:蓝海资料掘金营,微信deep3321