了解CPU

一个典型的 CPU 核心(Core)内部由许多协同工作的组件组成,主要可以分为 运算单元控制单元存储单元 三大部分。


1. 运算单元:ALU(Arithmetic Logic Unit)

加法器只是 ALU 的一部分。ALU 是 CPU 的“大脑实验室”,它负责所有的数学和逻辑判断:


2. 控制单元:CU(Control Unit)

如果 ALU 是“工人”,那么控制单元就是“工头”。它不干具体的活,但负责指挥:


3. 存储单元:寄存器(Registers)

这是 CPU 内部速度最快、成本最高的“记事本”。


4. 现代 CPU 的“特种部队”

除了上述经典架构,现代 CPU 还有很多强大的辅助模块:


总结:CPU 的“指挥体系”

你可以把 CPU 想象成一个高效的工厂流水线:

  1. PC 告诉你去哪拿图纸(取指)。
  2. Decoder 翻译图纸(译码)。
  3. Registers 准备好原材料(取数)。
  4. ALU 开始干活,可能是加法器,也可能是位移器(执行)。
  5. Status Register 汇报干得怎么样(更新状态)。

时钟频率: 时钟频率就是“快门”按下的频率,而 CPU 就像是在拍一部极其高速的定格动画。

负数怎么表示? 或者怎么让CPU做减法?

让 CPU 只用“加法器”就能做减法,从而简化硬件设计。

True Form(原码), One’s Complement(反码), Two’s Complement(补码)

NumberTrue FormOne’s complementTwo’s complement
7011101110111
0000011110000
-3101111001101
1000100010001
-1100111101111

反码与补码总结

补码的本质是同余运算, 为什么要加1?先只从数学角度思考

1. 补码的本质:找“补数”

在数学上,补码(Two’s Complement)的逻辑是:负数 $x$ 的补码 = 模 $M - |x|$

想象一个只有 4 位的计算机,它能表示的最大状态数是 $2^4 = 16$。这里的 16 就是“模” ($M$)。 如果你想计算 $3 - 7$:


2. 为什么会有“加 1”这个动作?

既然公式是 $M - |x|$,那么在二进制里怎么实现这个减法呢?

假设我们要算 $-7$ 的 8 位补码:

直接做这个减法很麻烦,因为涉及连续借位。数学家发现了一个技巧:

  1. 先用 “全 1” 的数去减(即 $2^8 - 1 = 255$,二进制是 1111 1111)。
  2. 1111 1111 - 0000 0111 = 1111 1000

注意: 用全 1 去减,等同于按位取反(这就是反码的来源)。

  1. 但我们要的是减去 $256$,而刚才只减去了 $255$,还差一个 $1$
  2. 所以,最后必须把这欠下的 $1$ 加回去

结论:


3. “加 1”解决的致命问题:正负零

如果不加这个 1(只停留在反码阶段),会出现一个数学灾难:

在 4 位系统中:

当我们加上 1 后: 1111 (反码) $+ 1 = 10000$。 在 4 位系统里,最高位的 1 溢出被丢弃,结果变成了 0000


4. 总结

“加 1”这个动作,在逻辑上是为了补全模运算的缺口,在工程上是为了消除“负零”,并让整个数轴可以连续。

正是因为这多出来的“加 1”,补码系统比原码多出了一个可以表示的数字:

你想看看 $-128$ 这个特殊的补码在内存里长什么样吗?(它在 8 位系统里非常特殊,取反加一后还是它自己)。

位运算(Go / Java)

常见位运算:按位与、按位或、按位异或 、按位取反、左移、右移

按位与 &、按位或 |、按位异或 ^、按位取反(Go 用一元 ^、Java 用 ~)、左移 <<、右移 >>(算术右移,保留符号位)。Java 另有无符号右移 >>>;在 Go 中可通过先转换为无符号类型再右移来实现逻辑右移。

常用技巧:

示例 — Go:

package main

import "fmt"

func main() {
		var a int32 = 5    // 00000101
		var b int32 = -5   // 补码表示

		fmt.Printf("a = %08b %d\n", a, a)
		fmt.Printf("b = %08b %d\n", b, b)
		fmt.Printf("a&b = %08b %d\n", a&b, a&b)
		fmt.Printf("a|b = %08b %d\n", a|b, a|b)
		fmt.Printf("a^b = %08b %d\n", a^b, a^b)
		fmt.Printf("^a = %08b %d\n", ^a, ^a) // 一元 ^ 为按位取反
		fmt.Printf("a<<1 = %08b %d\n", a<<1, a<<1)
		fmt.Printf("b>>1 = %08b %d\n", b>>1, b>>1) // 算术右移(保留符号)
		fmt.Printf("uint32(b)>>1 = %08b %d\n", uint32(b)>>1, uint32(b)>>1) // 逻辑右移示例
}

示例 — Java:

public class Bits {

	public static void main(String[] args) {
		int a = 5;
		int b = -5;
		System.out.println(Integer.toBinaryString(a));
		System.out.println(Integer.toBinaryString(b));
		System.out.println("a&b: " + (a & b));
		System.out.println("a|b: " + (a | b));
		System.out.println("a^b: " + (a ^ b));
		System.out.println("~a: " + (~a)); // 按位取反
		System.out.println("a<<1: " + (a << 1));
		System.out.println("a>>1: " + (a >> 1)); // 算术右移
		System.out.println("a>>>1: " + (a >>> 1)); // 逻辑右移
	}
}

示例:用补码得到负数

以上示例可直接复制到本地运行以观察二进制表示与位运算效果。

浮点数的精度与高精度计算

浮点数存储和计算的误差

浮点数(float/double)在存储和计算时都不是完全精确的:

如何实现精确计算

  1. 定点数方案:将金额等数据放大为整数(如元转为分),全部用整数类型计算,最后再缩放回去。
  2. 高精度库:如 Python 的 decimal.Decimal、Java 的 BigDecimal、Go 的 math/big 包等,支持任意精度小数运算,避免二进制误差。
  3. 避免直接用 float/double 进行金额、计数等敏感计算。

浮点数转高精度类型的注意事项

Java 示例

// 推荐:用字符串构造,精确
BigDecimal bd1 = new BigDecimal("0.1");

// 不推荐:用 double 构造,可能有误差
BigDecimal bd2 = new BigDecimal(0.1);

// 设置精度和四舍五入
bd1 = bd1.setScale(2, RoundingMode.HALF_UP); // 保留2位小数,四舍五入

Java 默认不会自动四舍五入,只有调用 setScale 并指定舍入模式时才会四舍五入。

Go 示例

import "math/big"

// 推荐:用字符串构造,精确
f, _, _ := big.NewFloat(0).Parse("0.1", 10)

// 不推荐:用 float64 构造,可能有误差
f2 := big.NewFloat(0.1)

// 设置精度
f.SetPrec(53) // 53位二进制精度(约16位十进制)
// 格式化输出
f.Text('f', 2) // 保留2位小数

Go 构造 big.Float 时不会自动四舍五入,需手动设置精度和格式化输出。

简化建议


浮点数用于表示小数和极大/极小的实数范围。现代计算机普遍采用 IEEE 754 标准。

1. 浮点数的结构(以 32 位单精度为例)

二进制结构:

| S |   E   |         M         |
|---|-------|-------------------|
| 1 | 8 bit |     23 bit        |

数值计算公式:

$$ (-1)^S \times 1.M \times 2^{E-127} $$

2. 特殊值

3. 常见陷阱

4. 示例

以十进制 0.15625 为例:

  1. 转二进制:$0.00101$
  2. 规格化:$1.01 \times 2^{-3}$
  3. S=0, E=124 (127-3), M=010…0

5. 扩展

double 近似值的唯一性和固定性

为什么 0.1 这样的十进制字面量在 double 类型下总是“固定”的?

结论: 因为 double 的存储格式和转换算法是标准且唯一的,所以同一个十进制字面量总会得到同一个 double 近似值。