二进制与 Go 的原子操作
前置阅读:
二进制相关基础概念
有符号二进制整数有正数和负数。在 x86 处理器中,MSB 表示的是符号位:0 表示正数,1 表示负数。下图展示了 8 位的正数和负数:
概念总结:
- 反码、补码是二进制的一种表现形式;
- 在计算机内所有数值底层都用补码表示,无论正负数(十进制);
- 如果一串二进制值需要视为数值则需要将其视为补码;
- 反码是十进制转二进制计算的一个过程即对一个十进制取补码的过程,一般用在负数转换规则上;
- 反码可以通过二进制值按位取反得到(所有二进制位都取反);
- 正数(十进制)的补码是其二进制本身,负数(十进制)的补码是十进制负数的绝对值求补码后取反码加一;
表示正数的补码可以直接转成十进制,表示负数的补码想要转回十进制步骤如下:
- 对表示负数的补码取反码加一得到负数的十进制绝对值补码;
- 再将负数的十进制绝对值补码转成十进制得到负数的十进制绝对值;
- 最后加上符号位;
- 无论是正数加正数(十进制加法)还是正数/负数加负数(十进制减法)都可以用补码加补码表示;
- 一个值的正数的补码与其负数的补码相加等于 0;
反码
反码可以通过二进制值按位取反得到(所有二进制位都取反)
正数的反码示例:
十进制数值 | 补码 | 反码 |
---|---|---|
0 | 0000 0000 | 1111 1111 |
1 | 0000 0001 | 1111 1110 |
2 | 0000 0010 | 1111 1101 |
3 | 0000 0011 | 1111 1100 |
4 | 0000 0100 | 1111 1011 |
负数的反码示例:
十进制数值 | 补码 | 反码 |
---|---|---|
-0 | 0000 0000 | 1111 1111 |
-1 | 1111 1111 | 0000 0000 |
-2 | 1111 1110 | 0000 0001 |
-3 | 1111 1101 | 0000 0010 |
补码(十进制转二进制)
在计算机内所有数值底层都用补码表示,无论正负数(十进制)
十进制数值 | 补码 |
---|---|
0 | 0000 0000 |
1 | 0000 0001 |
2 | 0000 0010 |
3 | 0000 0011 |
-0 | 0000 0000 |
-1 | 1111 1111 |
-2 | 1111 1110 |
-3 | 1111 1101 |
负数补码计算过程示例:
十进制数值 | 绝对值 | 绝对值补码 | 绝对值补码取反 | 绝对值补码取反加一 | 正确补码 | 十进制数值 |
---|---|---|---|---|---|---|
-0 | 0 | 0000 0000 | 1111 1111 | 1111 1111 + 1 ————— 1,0000 0000 |
0000 0000 | -0 |
-1 | 1 | 0000 0001 | 1111 1110 | 1111 1110 + 1 ————— 1111 1111 |
1111 1111 | -1 |
-2 | 2 | 0000 0010 | 1111 1101 | 1111 1101 + 1 ————— 1111 1110 |
1111 1110 | -2 |
-3 | 3 | 0000 0011 | 1111 1100 | 1111 1100 + 1 ————— 1111 1101 |
1111 1101 | -3 |
-4 | 4 | 0000 0100 | 1111 1011 | 1111 1011 + 1 ————— 1111 1100 |
1111 1100 | -4 |
-5 | 5 | 0000 0101 | 1111 1010 | 1111 1010 + 1 ————— 1111 1011 |
1111 1011 | -5 |
补码(二进制转十进制)
表示正数的补码可以直接转成十进制,表示负数的补码想要转回十进制步骤如下:
- 对表示负数的补码取反码加一得到负数的十进制绝对值补码;
- 再将负数的十进制绝对值补码转成十进制得到负数的十进制绝对值;
- 最后加上符号位;
MSB | 补码 | 十进制数值 |
---|---|---|
0 | 0000 0000 | 0 |
0 | 0000 0001 | 1 |
0 | 0000 0010 | 2 |
0 | 0000 0011 | 3 |
0 | 0000 0100 | 4 |
0 | 0000 0101 | 5 |
1 | 1111 1111 | -1 |
1 | 1111 1110 | -2 |
1 | 1111 1101 | -3 |
1 | 1111 1100 | -4 |
1 | 1111 1011 | -5 |
负数转换示例:
MSB | 补码 | 补码取反 | 补码取反加一 | 补码取反加一后所代表十进制值 | 符号 | 十进制结果 | 补码 |
---|---|---|---|---|---|---|---|
1 | 1111 1111 | 0000 0000 | 0000 0001 | 1 | - | -1 | 1111 1111 |
1 | 1111 1110 | 0000 0001 | 0000 0010 | 2 | - | -2 | 1111 1110 |
1 | 1111 1101 | 0000 0010 | 0000 0011 | 3 | - | -3 | 1111 1101 |
1 | 1111 1100 | 0000 0011 | 0000 0100 | 4 | - | -4 | 1111 1100 |
1 | 1111 1011 | 0000 0100 | 0000 0101 | 5 | - | -5 | 1111 1011 |
补码相加
无论是正数加正数(十进制加法)还是正数/负数加负数(十进制减法)都可以用补码加补码表示
正数加正数的补码计算过程示例:
表达式 | 补码相加 | 二进制结果 | 十进制结果 |
---|---|---|---|
0+0 | 0000 0000 + 0000 0000 —————— 0000 0000 |
0000 0000 | 0 |
0+1 | 0000 0000 + 0000 0001 —————— 0000 0001 |
0000 0001 | 1 |
1+1 | 0000 0001 + 0000 0001 —————— 0000 0010 |
0000 0010 | 2 |
2+1 | 0000 0010 + 0000 0001 —————— 0000 0011 |
0000 0011 | 3 |
正数加负数的补码计算过程示例:
表达式 | 补码相加 | 二进制结果 | 十进制结果 |
---|---|---|---|
0+(-0) | 0000 0000 + 0000 0000 —————— 0000 0000 |
0000 0000 | 0 |
0+(-1) | 0000 0000 + 1111 1111 —————— 1111 1111 |
1111 1111 | -1 |
1+(-1) | 0000 0001 + 1111 1111 —————— 1,0000 0000 |
0000 0000 | 0 |
1+(-2) | 0000 0001 + 1111 1110 —————— 1111 1111 |
1111 1111 | -1 |
2+(-2) | 0000 0010 + 1111 1110 —————— 1,0000 0000 |
0000 0000 | 0 |
2+(-1) | 0000 0010 + 1111 1111 —————— 1,0000 0001 |
0000 0001 | 1 |
负数加负数的补码计算过程示例:
表达式 | 补码相加 | 二进制结果 | 十进制结果 |
---|---|---|---|
(-0)+(-0) | 0000 0000 + 0000 0000 —————— 0000 0000 |
0000 0000 | 0 |
(-1)+(-1) | 1111 1111 + 1111 1111 —————— 1,1111 1110 |
1111 1110 | -2 |
(-1)+(-2) | 1111 1111 + 1111 1110 —————— 1,1111,1101 |
1111 1101 | -3 |
二进制、反码、补码
同样的一串二进制数字,即可以是反码也可以是补码,如果是补码则其可以通过上述规则转成对应的十进制数值,如果是反码则代表其为计算过程中间值,如果想知道反码在十进制中所表示的数值,可以将其视为补码再通过上述规则转成十进制即可。
正数示例:
十进制数值 x | x 取补码 fn1(x)=a | x 取反码 fn2(x)=b | b 的十进制形式 y |
---|---|---|---|
0 | 0000 0000 | 1111 1111 | -1 |
1 | 0000 0001 | 1111 1110 | -2 |
2 | 0000 0010 | 1111 1101 | -3 |
3 | 0000 0011 | 1111 1100 | -4 |
4 | 0000 0100 | 1111 1011 | -5 |
负数示例:
十进制数值 x | x 取补码 fn1(x)=a | x 取反码 fn2(x)=b | b 的十进制形式 y |
---|---|---|---|
-0 | 0000 0000 | 1111 1111 | -1 |
-1 | 1111 1111 | 0000 0000 | 0 |
-2 | 1111 1110 | 0000 0001 | 1 |
-3 | 1111 1101 | 0000 0010 | 2 |
示例汇总:
十进制数值 x | x 取补码 fn1(x)=a | x 取反码 fn2(x)=b | b 的十进制形式 y | y + 1 | 十进制数值 x |
---|---|---|---|---|---|
0 | 0000 0000 | 1111 1111 | -1 | 0 | 0 |
1 | 0000 0001 | 1111 1110 | -2 | -1 | 1 |
2 | 0000 0010 | 1111 1101 | -3 | -2 | 2 |
3 | 0000 0011 | 1111 1100 | -4 | -3 | 3 |
-0 | 0000 0000 | 1111 1111 | -1 | 0 | -0 |
-1 | 1111 1111 | 0000 0000 | 0 | 1 | -1 |
-2 | 1111 1110 | 0000 0001 | 1 | 2 | -2 |
-3 | 1111 1101 | 0000 0010 | 2 | 3 | -3 |
通过该表格示例可以得出以下两个规律:
规律 一
反码所表示的数值与原数值之间规律如下(y 代表反码之后的十进制值):
- fn2(x) = -x-1
- fn2(x) + 1 = -x
- y = -x-1
- y +1 = -x
即如果想得到一个十进制正数值的负数形式(1 => -1)或则得到一个十进制负数值的正数形式可以通过对原值取反码加一得到:
十进制数值 x | 十进制取反 -x | 过程 |
---|---|---|
0 | 0 | 取反码(0)+1 = -1+1 |
1 | -1 | 取反码(1)+1 = -2+1 |
2 | -2 | 取反码(2)+1 = -3+1 |
3 | -3 | 取反码(3)+1 = -4+1 |
-1 | 1 | 取反码(-1)+1 = 0+1 |
-2 | 2 | 取反码(-2)+1 = 1+1 |
-3 | 3 | 取反码(-3)+1 = 2+1 |
规律 二
将示例汇总表格再进一步简化:
十进制数值 x | x 的反码十进制表示形式 y | 翻译 -1 | 翻译 -2 |
---|---|---|---|
0 | -1 | 0 的反码是 -1 | -1 是 0 的反码 |
1 | -2 | 1 的反码是 -2 | -2 是 1 的反码 |
2 | -3 | 2 的反码是 -3 | -3 是 2 的反码 |
3 | -4 | 3 的反码是 -4 | -4 是 3 的反码 |
-0 | -1 | -0 的反码是 -1 | -1 是 -0 的反码 |
-1 | 0 | -1 的反码是 0 | 0 是 -1 的反码 |
-2 | 1 | -2 的反码是 1 | 1 是 -2 的反码 |
-3 | 2 | -3 的反码是 2 | 2 是 -3 的反码 |
可以看出在十进制格式下,原数值与反码的关系:
- 如果我需要 -1 我可以用 0 的反码代替;
- 如果我需要 -4 我可以用 3 的反码代替;
规律:
- x = |y| -1
- x + y = -1
Go 的表现
二进制的输出格式
在 Go 语言中,一个数值是正数或负数,无论是何种打印方式,输出的都会待上正负号:
fmt.Printf("1 的十进制 : %v\n",1)
fmt.Printf("-1 的十进制 : %v\n",-1)
fmt.Printf("-1 的二进制(简化版): %v\n",strconv.FormatInt(-1,2))
fmt.Printf("1 的二进制 : %064b\n",1) // 占 64 位宽,不足补 0
fmt.Printf("-1 的二进制 : %064b\n",-1) // 占 64 位宽,不足补 0
fmt.Printf("4 的十进制 : %v\n",4)
fmt.Printf("-4 的十进制 : %v\n",-4)
fmt.Printf("-4 的二进制(简化版): %v\n",strconv.FormatInt(-4,2))
fmt.Printf("4 的二进制 : %064b\n",4) // 占 64 位宽,不足补 0
fmt.Printf("-4 的二进制 : %064b\n",-4) // 占 64 位宽,不足补 0
// 输出
// 1 的十进制 : 1
// -1 的十进制 : -1
// -1 的二进制(简化版): -1
// 1 的二进制 : 0000000000000000000000000000000000000000000000000000000000000001
// -1 的二进制 : -000000000000000000000000000000000000000000000000000000000000001
// 4 的十进制 : 4
// -4 的十进制 : -4
// -4 的二进制(简化版): -100
// 4 的二进制 : 0000000000000000000000000000000000000000000000000000000000000100
// -4 的二进制 : -000000000000000000000000000000000000000000000000000000000000100
- 可以看出输出二进制时 Go 的输出与十进制一样同样将符号位具象化,而非输出对应的 0 或 1;
- 输出二进制时数值部分则取其绝对值的补码;
如果我们想要看到正确的负数的补码形式则需要通过无符号数值类型间接实现:
- 无符号数值类型如何表示一个数的负数形式,答案是补码取反码加一;
- 譬如如何用无符号数值类型表示 -1:
^uint8(1) + 1
; - ^ 符号在二元运算中代表亦或符;在一元运算中代表取反码符;
fmt.Printf("int8(1) : %08b \n", int8(1)) // 占 8 位宽,不足补 0
fmt.Printf("^int8(1) : %08b \n", ^int8(1)) // 占 8 位宽,不足补 0
fmt.Printf("^int8(1)+1 : %08b \n", ^int8(1)+1) // 占 8 位宽,不足补 0
fmt.Printf("uint8(1) : %08b \n", uint8(1)) // 占 8 位宽,不足补 0
fmt.Printf("^uint8(1) : %08b \n", ^uint8(1)) // 占 8 位宽,不足补 0
fmt.Printf("^uint8(1)+1 : %08b \n", ^uint8(1)+1)// 占 8 位宽,不足补 0
// 输出
// int8(1) : 00000001
// ^int8(1) : -0000010
// ^int8(1)+1 : -0000001
// uint8(1) : 00000001
// ^uint8(1) : 11111110
// ^uint8(1)+1 : 11111111
- 可以看到通过使用无符号数值类型对 1 取反后得到的是 -2 的补码形式
1111 1110
,接着对反码后加一得到原值 1 的负数形式 -1 的补码1111 1111
;
Go 的原子操作
无论是有符号数值类型还是无符号数值类型,只要转成补码后进行的计算过程不需要考虑符号位的问题。
A - B = A + ( -B )
A - B = 补码( A ) + 补码( -B )
原子操作即是进行过程中不能被中断的操作。也就是说,针对某个值的原子操作在被进行的过程当中,CPU 绝不会再去进行其它的针对该值的操作。无论这些其它的操作是否为原子操作都会是这样。为了实现这样的严谨性,原子操作仅会由一个独立的 CPU 指令代表和完成。只有这样才能够在并发环境下保证原子操作的绝对安全。
Go 语言提供的原子操作都是非侵入式的。它们由标准库代码包 sync/atomic 中的众多函数代表。我们可以通过调用这些函数对几种简单的类型的值进行原子操作。这些类型包括 int32、int64、uint32、uint64、uintptr 和 unsafe.Pointer 类型,共 6 个。这些函数提供的原子操作共有 5 种,即:增或减、比较并交换、载入、存储和交换。
原子操作 - 增或减
相关文档如图所示:
这里主要需要注意的是 Uint 类型的原子操作,以 AddUint32 函数为例
原子性的增加数值:
value := uint32(1)
atomic.AddUint32(&value, 1)
fmt.Printf("after call atomic.AddUint32(&value, 1) value is: %v\n", value)
atomic.AddUint32(&value, 2)
fmt.Printf("after call atomic.AddUint32(&value, 2) value is: %v\n", value)
atomic.AddUint32(&value, 3)
fmt.Printf("after call atomic.AddUint32(&value, 3) value is: %v\n", value)
// 输出
// after call atomic.AddUint32(&value, 1) value is: 2
// after call atomic.AddUint32(&value, 2) value is: 4
// after call atomic.AddUint32(&value, 3) value is: 7
原子性的减少数值:
如文档所述,如果需要减去一个正数 c 需要通过 ^uint32(c-1)
计算得到 c 的补码。
const one, two, three = 1, 2, 3
value := uint32(10)
atomic.AddUint32(&value, ^uint32(one - 1)) // 减一
fmt.Printf("after callatomic.AddUint32(&value, ^uint32(one - 1)) value is: %v\n", value)
atomic.AddUint32(&value, ^uint32(two - 1)) // 减二
fmt.Printf("after callatomic.AddUint32(&value, ^uint32(two - 1)) value is: %v\n", value)
atomic.AddUint32(&value, ^uint32(three - 1)) // 减三
fmt.Printf("after callatomic.AddUint32(&value, ^uint32(three - 1)) value is: %v\n", value)
// 输出
// after callatomic.AddUint32(&value, ^uint32(one - 1)) value is: 9
// after callatomic.AddUint32(&value, ^uint32(two - 1)) value is: 7
// after callatomic.AddUint32(&value, ^uint32(three - 1)) value is: 4
- value -1 等价于 value + (-1) ,等价于 补码( value ) + 补码( -1 );
- 通过前面二进制规律二 得知,求 -1 的补码相当于求 0 的反码;
- go 的反码运算符位
^
; - 结合后便可实现了无符号类型数据的减法运算。