位运算卡常技巧详解
位运算卡常技巧详解
位运算的核心优势在于:它直接在二进制比特位上操作,避免了高级语言抽象带来的开销,一条指令就能完成多个比特位的并行计算。以下是七种常用且高效的位运算优化技巧。
1. 乘除2的幂:移位代替乘除
-
技巧: 用左移(
<<
)和右移(>>
)操作代替乘以或除以2的幂次方。 -
原理: 整数在计算机中以二进制形式存储。左移一位等价于乘以2,右移一位等价于除以2(向下取整)。移位指令的执行速度远快于乘法指令。
-
示例:
// 优化前 int a = x * 32; int b = y / 8; int c = z % 4; // 求余也慢// 优化后 int a = x << 5; // 2^5 = 32 int b = y >> 3; // 2^3 = 8 int c = z & 3; // 见技巧2,对2^n取余等价于与(2^n - 1)进行与操作
2. 对2的幂取模:与操作代替取模
-
技巧: 用按位与(
&
)操作代替对2的幂次方的取模(%
)运算。 -
原理: 一个数
x
对2^n
取模,实质就是获取x
的低n
位二进制值。这与和掩码(2^n - 1)
进行按位与操作的效果完全相同。 -
示例:
// 优化前 int remainder = x % 16; // 求x除以16的余数// 优化后 int remainder = x & 15; // 15的二进制是 1111,保留最后4位 // 同样适用于哈希表桶数量的计算,如果桶数bucket_count是2的n次方 int bucket_index = hash_value % bucket_count; // 慢 int bucket_index = hash_value & (bucket_count - 1); // 快!
3. 判断奇偶性:与1代替模2
-
技巧: 用
x & 1
代替x % 2
来判断奇偶性。 -
原理: 二进制的最低位为1则是奇数,为0则是偶数。
x & 1
直接取出最低位,比取模运算快得多。 -
示例:
// 优化前 if (x % 2 == 1) { /* 是奇数 */ } if (x % 2 == 0) { /* 是偶数 */ }// 优化后 if (x & 1) { /* 是奇数 */ } if (!(x & 1)) { /* 是偶数 */ } // 或 if ((x & 1) == 0)
4. 交换两个变量的值:异或交换
-
技巧: 使用异或(
^
)操作在不引入临时变量的情况下交换两个整数。 -
原理: 利用异或操作的性质:
a ^ b ^ b = a
。- 第一步:
a = a ^ b
- 第二步:
b = a ^ b
->b = (a ^ b) ^ b = a
- 第三步:
a = a ^ b
->a = (a ^ b) ^ a = b
- 第一步:
-
注意: 这是一个著名的技巧,但在现代CPU上,由于指令级并行和流水线技术,使用临时变量的方法通常更快,因为异或交换串行依赖严重。它更适用于寄存器紧张或作为趣味编程。
-
示例:
// 传统方法(编译器可能已经优化得很好了) void swap(int &a, int &b) {int temp = a;a = b;b = temp; }// 异或交换方法 void swap_xor(int &a, int &b) {a ^= b;b ^= a;a ^= b; }
5. 判断两数符号是否相同:异或代替乘法比较
-
技巧: 用
(a ^ b) >= 0
来判断两个整数a
和b
是否同号(即都是正数或都是负数)。 -
原理: 整数的最高位是符号位(0正1负)。异或操作的特点是“相同为0,不同为1”。如果
a
和b
符号相同,则它们最高位的异或结果为0,整个a ^ b
的结果必然是一个非负数(>=0);反之则为负数(<0)。 -
示例:
// 优化前 if (a * b > 0) { // 可能溢出!且乘法慢// 同号 }// 优化后(安全且快速) if ((a ^ b) >= 0) {// 同号 }
6. 取绝对值:无分支位运算
-
技巧: 使用位运算实现无分支(Branchless)的绝对值计算,避免
if
判断带来的分支预测失败风险。 -
原理:
int mask = x >> 31;
: 对于32位有符号整数,算术右移31位会将符号位扩展到所有位。如果x
是负数,mask
为0xFFFFFFFF
(即-1);如果x
是非负数,mask
为0
。(x ^ mask) - mask;
:- 如果
x
是负数(mask = -1
):(x ^ -1) - (-1)
。x ^ -1
是对x
按位取反,然后再+1
,这正好是补码定义中求负数的方法,结果就是-x
。 - 如果
x
是非负数(mask = 0
):(x ^ 0) - 0 = x
。
- 如果
-
示例:
// 优化前(有分支) int abs_val = (x < 0) ? -x : x;// 优化后(无分支,对流水线友好) int abs_val = (x ^ (x >> 31)) - (x >> 31); // 或者另一种常见写法: int mask = x >> 31; int abs_val = (x + mask) ^ mask;
7. 快速计算二进制中1的个数(PopCount):使用内置函数
-
技巧: 使用编译器内置函数
__builtin_popcount
(GCC/Clang) 或_mm_popcnt_u32
(Intel Intrinsics)。 -
原理: 现代CPU(自SSE4.2起)有专门的
POPCNT
指令来执行这个操作。一条指令就能完成计算,速度远超任何手动实现的算法(如Brian Kernighan算法)。这是“用硬件指令降维打击”的典范。 -
示例:
// 手动实现(Brian Kernighan算法,已很快,但仍不如硬件指令) int count_set_bits(int n) {int count = 0;while (n) {n &= (n - 1); // 清除最低位的1count++;}return count; }// 优化后(终极速度) int count = __builtin_popcount(x); // 对于GCC/Clang // #include <nmmintrin.h> // int count = _mm_popcnt_u32(x); // 对于MSVC和ICC等,需要包含头文件// 对于long long类型,使用 __builtin_popcountll
总结与注意事项
技巧 | 优化前 | 优化后 | 原理 |
---|---|---|---|
乘除2的幂 | x * 32 , y / 8 | x << 5 , y >> 3 | 移位指令速度快 |
对2^n取模 | x % 16 | x & 15 | 取模等价于取低位 |
判断奇偶性 | x % 2 == 0 | (x & 1) == 0 | 直接检查最低位 |
判断符号相同 | a * b > 0 | (a ^ b) >= 0 | 检查符号位异或结果 |
取绝对值 | x < 0 ? -x : x | (x^mask)-mask | 无分支,避免预测失败 |
统计1的个数 | 循环清除最低位 | __builtin_popcount | 使用专用CPU指令 |
重要提醒:
- 可读性: 位运算会严重降低代码可读性。务必添加清晰的注释,说明你在做什么以及为什么这么做。
- 适用范围: 这些技巧主要针对整数且有符号数的移位操作是实现定义(Implementation-defined) 的(通常使用算术移位)。确保你的操作在算术和逻辑上都正确。对于无符号数,移位总是逻辑移位,更安全。
- 编译器优化: 现代编译器在开启高优化等级(如
-O2
/-O3
)时,通常能自动将x * 2
优化为x << 1
。手动优化的意义更多在于编写编译器无法确定的复杂逻辑,或者在某些编译器优化能力较弱的场合。
熟练并合理地运用这些位运算技巧,能让你的程序在性能关键的循环中显著提升速度。