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 = 0x ^ 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 知识体系总览

下面这张图,把本章的核心知识点串起来了。你可以把它当作一个速查地图。

位运算技巧 替代乘除法 左移 << → 乘以2^n 右移 >> → 除以2^n 注意:仅无符号/正数 判断奇偶 x & 1 == 0 → 偶数 x & 1 == 1 → 奇数 比 % 运算快一个数量级 交换变量 a ^= b; b ^= a; a ^= b; 三步异或完成交换 ⚠ 避免自交换 位掩码操作(寄存器控制) 置位: reg |= (1 << n) | 清零: reg &= ~(1 << n) 翻转: reg ^= (1 << n) | 读取: (reg >> n) & 1

8.6 总结一下

位运算不是什么高深莫测的东西,它就是用CPU最擅长的方式跟硬件对话

  • 乘除2的幂 → 用移位
  • 判断奇偶 → 用 & 1
  • 交换变量 → 用异或(注意避坑)
  • 寄存器操作 → 用位掩码

你想想看,这些技巧加起来,可能让你的关键代码提速2~5倍,而且不花一分钱硬件成本。这种优化,才是真正的“性价比之王”。

我的建议: 平时写代码时,养成“先想位运算”的习惯。哪怕最后没用上,这个思考过程也会让你对数据在内存中的表示越来越敏感。久而久之,你看到数字,脑子里浮现的是二进制——那时候,你就真的入门了。


公众号:蓝海资料掘金营,微信deep3321