位运算
位运算是一种对二进制数据进行操作的技术。
常用的位运算符:
-
与运算(AND)
a & b
:对应位都为 1 时,结果为 1,否则为 0 -
或运算(OR)
a | b
:对应位只要有一个为 1,结果为 1 -
异或运算(XOR)
a ^ b
:对应位不同,结果为 1,相同为 0 -
非运算(NOT)
~a
:将每个位取反,0 变 1,1 变 0 -
左移运算(Left Shift)
a << n
:将二进制位向左移动指定的位数,右侧补 0 -
右移运算(Right Shift)
a >> n
:将二进制位向右移动指定的位数,左侧补 0
基础示例:
contract Demo {
// 与运算
function andOperation(uint8 a, uint8 b) public pure returns (uint8) {
return a & b;
}
// 或运算
function orOperation(uint8 a, uint8 b) public pure returns (uint8) {
return a | b;
}
// 异或运算
function xorOperation(uint8 a, uint8 b) public pure returns (uint8) {
return a ^ b;
}
// 非运算
function notOperation(uint8 a) public pure returns (uint8) {
return ~a;
}
// 左移运算
function leftShiftOperation(uint8 a, uint8 n) public pure returns (uint8) {
return a << n;
}
// 右移运算
function rightShiftOperation(uint8 a, uint8 n) public pure returns (uint8) {
return a >> n;
}
}
-
与运算示例:
5 & 3
,二进制表示为0101 & 0011
,结果为0001
,即1
-
或运算示例:
5 | 3
,二进制表示为0101 | 0011
,结果为0111
,即7
-
异或运算示例:
5 ^ 3
,二进制表示为0101 ^ 0011
,结果为0110
,即6
-
非运算示例:
~5
,二进制表示为~00000101
(对于 8 位无符号整数),结果为11111010
,即250
-
左移运算示例:
5 << 1
,二进制表示为0101 << 1
,结果为1010
,即10
-
右移运算示例:
5 >> 1
,二进制表示为0101 >> 1
,结果为0010
,即2
应用示例
将某数值转为二进制表示:
contract Demo {
// 将无符号整数转换为二进制字符串表示
function toBinary(uint num) public pure returns (string memory) {
// 如果输入数字为 0, 直接返回字符串 "0"
if (num == 0) {
return "0";
}
// 计算二进制表示所需的位数
uint length = 0;
uint temp = num;
while (temp > 0) {
length++;
temp >>= 1; // 将 temp 右移一位, 相当于除以 2
}
// 创建一个长度为 length 的字节数组, 用于存储二进制表示
bytes memory binary = new bytes(length);
// 循环遍历每一位
for (uint i = 0; i < length; i++) {
// (1 << i) 将 1 左移 i 位, 得到一个只有第 i + 1 位为 1 的数
// num & (1 << i) 能检查 num 的第 i + 1 位是否为 1
// 如果结果不为 0, 则第 i 位为 1, 否则为 0
// eg: i = 3, 1 << 3 = 00001000, 00001101 & 00001000 = 00001000 != 0
binary[length - i - 1] = (num & (1 << i)) != 0
? bytes1("1")
: bytes1("0");
}
// 将字节数组转换为字符串并返回
return string(binary);
}
}
获取某二进制数值的最后 n 位:eg:x = 00001101 = 13
,getLastBits(x, 3) = 00000101 = 5
contract Demo {
// 思路 1: 定义一个掩码, 掩码的最后 n 位为 1, 其余位为 0, 与原数值进行 & 运算
function getLastBits(uint8 x, uint8 n) public pure returns (uint8) {
// 生成掩码:
// (1 << n) 将 1 左移 n 位, 得到一个只有第 n + 1 位为 1 的数
// (1 << n) - 1 得到一个只有最后 n 位为 1 的数
// eg: n = 3, 1 << 3 = 00001000, 00001000 - 1 = 00000111
uint8 mask = (uint8(1) << n) - uint8(1);
return x & mask;
}
// 思路 2: 定义一个除数, 除数的倒数第 n + 1 位为 1, 其余位为 0, 对原数值进行 % 运算
function getLastBits2(uint8 x, uint8 n) public pure returns (uint8) {
// 生成除数: 2 ** n 或 1 << n
// eg: n = 3, 2 ** 3 = 8 = 00001000; 1 << 3 = 00001000
uint8 divisor = uint8(1) << n;
return x % divisor;
}
}
查找最高有效位:最高有效位(Most Significant Bit,MSB)是二进制表示中最左边的 1 所在的位置。可以使用位运算来查找一个数值的最高有效位。
contract Demo {
// 查找 uint 数值的最高有效位; eg: 42 -> 101010 -> 5; 10 -> 001010 -> 3
function findMSBByIf(uint value) public pure returns (uint8) {
uint8 msb;
if (value >= 2 ** 128) {
value >>= 128;
msb += 128;
}
if (value >= 2 ** 64) {
value >>= 64;
msb += 64;
}
if (value >= 2 ** 32) {
value >>= 32;
msb += 32;
}
if (value >= 2 ** 16) {
value >>= 16;
msb += 16;
}
if (value >= 2 ** 8) {
value >>= 8;
msb += 8;
}
if (value >= 2 ** 4) {
value >>= 4;
msb += 4;
}
if (value >= 2 ** 2) {
value >>= 2;
msb += 2;
}
if (value >= 2 ** 1) {
// value >>= 1; 最后一个就不需要做位运算啦
msb += 1;
}
return msb;
}
}
我们可以用 assembly 优化上例,以节省 gas:
contract Demo {
function findMSBByIfInAsm(uint value) public pure returns (uint8) {
uint8 msb;
assembly {
// 如果 value 大于 2^128 - 1
if gt(value, 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF) {
value := shr(128, value) // 右移 128 位
msb := add(msb, 128) // msb 加 128
}
// 如果 value 大于 2^64 - 1
if gt(value, 0xFFFFFFFFFFFFFFFF) {
value := shr(64, value) // 右移 64 位
msb := add(msb, 64) // msb 加 64
}
// 如果 value 大于 2^32 - 1
if gt(value, 0xFFFFFFFF) {
value := shr(32, value) // 右移 32 位
msb := add(msb, 32) // msb 加 32
}
// 如果 value 大于 2^16 - 1
if gt(value, 0xFFFF) {
value := shr(16, value) // 右移 16 位
msb := add(msb, 16) // msb 加 16
}
// 如果 value 大于 2^8 - 1
if gt(value, 0xFF) {
value := shr(8, value) // 右移 8 位
msb := add(msb, 8) // msb 加 8
}
// 如果 value 大于 2^4 - 1
if gt(value, 0xF) {
value := shr(4, value) // 右移 4 位
msb := add(msb, 4) // msb 加 4
}
// 如果 value 大于 2^2 - 1
if gt(value, 0x3) {
value := shr(2, value) // 右移 2 位
msb := add(msb, 2) // msb 加 2
}
// 如果 value 大于 2^1 - 1
if gt(value, 0x1) {
// value := shr(1, value) 最后一个就不需要做位运算啦
msb := add(msb, 1) // msb 加 1
}
}
return msb;
}
}
我们可以使用循环来简化上例(但本例使用循环会消耗更多的 gas):
contract Demo {
function findMSBByLoop(uint value) public pure returns (uint8) {
uint8 msb
while (value > 0) {
value >>= 1; // 将 value 右移一位 (相当于除以 2)
msb++; // msb 加 1
}
return msb - 1; // 返回 msb 减 1, 因为最后一次循环多加了一次
}
}
使用 assembly 版本:
contract Demo {
function findMSBByLoopInAsm(uint value) public pure returns (uint8) {
uint8 msb;
assembly {
// 初始化 for 循环, 不需要初始化语句
for { } gt(value, 0) { msb := add(msb, 1) } {
// 循环条件: value > 0
// 每次循环结束时, msb 加 1
// 循环体: 将 value 右移一位(相当于除以 2)
value := div(value, 2)
}
// 循环结束后, msb 减 1, 因为最后一次循环多加了一次
msb := sub(msb, 1)
}
return msb;
}
}
其他实现方式:
contract Demo {
function findMSBInAsm(uint value) public pure returns (uint8) {
uint8 msb;
assembly {
let f := shl(7, gt(value, 0xffffffffffffffffffffffffffffffff))
// gt(value, 0xffffffffffffffffffffffffffffffff) 表示 value > 2^128 - 1 ? 1 : 0
// shl(7, gt(value, 0xffffffffffffffffffffffffffffffff)) 表示 value > 2^128 - 1 ? (1 << 7) : (0 << 7)
// 1 << 7 = 128, 0 << 7 = 0
value := shr(f, value)
// value >>= 128 or value >>= 0
msb := or(msb, f)
// 对于没有进位的简单加法场景, 可用 or 替代 add 以节省 gas
// eg: 0110 + 0001 = 0111, 0110 | 0001 = 0111
// 所以这里表示 msb += 128 or msb += 0
}
assembly {
let f := shl(6, gt(value, 0xffffffffffffffff))
// gt(value, 0xffffffffffffffff) 表示 value > 2^64 - 1 ? 1 : 0
// shl(6, gt(value, 0xffffffffffffffff)) 表示 value > 2^64 - 1 ? (1 << 6) : (0 << 6)
// 1 << 6 = 64, 0 << 6 = 0
value := shr(f, value)
// value >>= 64 or value >>= 0
msb := or(msb, f)
// msb += 64 or msb += 0
}
assembly {
let f := shl(5, gt(value, 0xffffffff))
// gt(value, 0xffffffff) 表示 value > 2^32 - 1 ? 1 : 0
// shl(5, gt(value, 0xffffffff)) 表示 value > 2^32 - 1 ? (1 << 5) : (0 << 5)
// 1 << 5 = 32, 0 << 5 = 0
value := shr(f, value)
// value >>= 32 or value >>= 0
msb := or(msb, f)
// msb += 32 or msb += 0
}
assembly {
let f := shl(4, gt(value, 0xffff))
value := shr(f, value)
msb := or(msb, f)
}
assembly {
let f := shl(3, gt(value, 0xff))
value := shr(f, value)
msb := or(msb, f)
}
assembly {
let f := shl(2, gt(value, 0xf))
value := shr(f, value)
msb := or(msb, f)
}
assembly {
let f := shl(1, gt(value, 0x3))
value := shr(f, value)
msb := or(msb, f)
}
assembly {
let f := gt(value, 0x1)
// value := shr(f, value) 最后一个就不需要做位运算啦
msb := or(msb, f)
}
return msb;
}
}
可见各方法及消耗的 gas 如下: findMSBByIf (1200)、findMSBByLoop (1662)、findMSBByIfInAsm (883)、findMSBByLoopInAsm (908)、findMSBInAsm (1010)