负数的计算机中的存储方式
在当代计算机体系结构(包括ARM、x86等)中,负数几乎全部采用“补码”(Two's Complement)形式存储。
这是数字电路设计上最优解,因为它将减法运算统一为了加法运算,省去了昂贵的减法器硬件。
为了彻底讲透,我们从三种编码方案的对比,深入到补码的数学本质和硬件溢出规则。
1. 三种二进制编码方案对比(以4位为例)
| 十进制 | 原码 (Sign-Magnitude) | 反码 (Ones' Complement) | 补码 (Two's Complement) |
|---|---|---|---|
| +7 | 0111 | 0111 | 0111 |
| +1 | 0001 | 0001 | 0001 |
| +0 | 0000 | 0000 | 0000 |
| -0 | 1000 | 1111 | 不存在 |
| -1 | 1001 | 1110 | 1111 |
| -7 | 1111 | 1000 | 1001 |
| -8 | 无法表示 | 无法表示 | 1000 |
为什么淘汰了前两种?
- 原码(符号位+数值位):存在“正零”和“负零”两个编码,浪费了状态;且做加法时符号位需要单独处理,电路极其复杂。
- 反码(按位取反):虽然解决了部分加减问题,但依然存在“负零”(1111),且加法进位时需要“循环进位(End-around carry)”,增加延迟。
- 补码(取反+1):消除了“负零”,使得 \(n\) 位二进制能表示的范围从 \([-(2^{n-1}-1), 2^{n-1}-1]\) 扩展到了 \([-2^{n-1}, 2^{n-1}-1]\)(例如4位可表示-8)。
2. 补码的数学本质:模运算(时钟原理)
补码的核心数学逻辑是模 \(2^n\) 同余。
对于一个 \(n\) 位的系统,任何数值 \(X\) 都会因为溢出而对 \(2^n\) 取模。负数的补码 \([X]_{补}\) 满足:
\[[X]_{补} = 2^n + X \quad (X < 0)
\]
举个直观的“时钟”例子(4位,模16):
假设你站在数字 0000(0点)上。
- 你想表示 -1,相当于顺时针往回走1格。往回走等价于向前走 15 格(因为 \(16-1=15\))。
- 因此,-1 的补码就是
1111(即十进制的15)。 - 同理,-2 对应 14(1110),-8 对应 8(1000)。
3. 补码的快速人工计算法
对于负数 \(X\),求其 \(n\) 位补码二进制:
- 写出 \(|X|\) 的二进制(原码)。
- 从右往左(从LSB到MSB)找第一个 '1'。
- 这个 '1' 不动,将这个 '1' 左边的所有位全部取反(0变1,1变0)。
示例:求 -6 的 4 位补码
- \(| -6 | = 6 = 0110\)。
- 从右往左看
0110,最低位是0(跳过),第二位是 1(找到)。 - 这个1保持不变,左边的
01取反变为10。 - 得到结果:
1010。
验证公式:\(2^4 - 6 = 16 - 6 = 10 = 1010_2\)。
4. 硬件视角:补码为什么天然适配加法器?
CPU内部只有加法器(Adder),没有减法器。补码让减法 \(A - B\) 变成了 \(A + (\text{补码}(B))\)。
验算:\(1 - 1 = 0\)(4位)
- 十进制:\(1 + (-1)\)
- 二进制:
0001+1111=10000。 - 由于寄存器只有4位,最高位的
1被自然溢出丢弃,最终结果为0000,完美计算正确!
验算:\(-8 - 1 = -9\)(4位)
- 二进制:\(1000 + 1111 = 10111\)。截取低4位为
0111(十进制+7)。 - 发生了负溢出(Negative Overflow)。因为4位补码最小只能表示 -8,-9 超出了范围,所以结果错误。在硬件中,状态寄存器(PSR)的 V位(溢出标志位) 会被置1。
5. SystemVerilog / 硬件设计中的实战要点
既然你在学习SystemVerilog和ARM,这块细节直接关乎RTL代码的正确性:
-
显式声明 signed:
logic signed [3:0] neg_val; // 声明为有符号 neg_val = -4'sd6; // 结果为 4'b1010 (-6) -
位宽匹配陷阱(最高位扩展):
当你将一个负数赋值给更大位宽的变量时,必须进行符号位扩展,而不是补零。- 错误:
4'b1010(-6) 赋给 8位 -> 补零变成0000_1010(+10),数值完全变了。 - 正确:
4'b1010赋给 8位 -> 符号位扩展变成1111_1010(-6)。 - SystemVerilog语法:使用
{ {4{neg_val[3]}}, neg_val }或直接赋值给signed变量(综合工具通常会自动扩展)。
- 错误:
-
无符号与有符号混合运算:
在Verilog/SV中,只要操作数中有一个是无符号的,整个表达式就被视为无符号运算,这会导致负数被当作极大的正数处理。logic signed [3:0] a = -1; // 1111 logic [3:0] b = 1; // 无符号 logic [3:0] c; c = a + b; // 结果:1111 + 0001 = 10000 -> 截断为 0000 (0) // 但如果你期望的是 (-1 + 1 = 0),结果虽然是0,但中间加法器是按无符号处理的, // 在处理比较操作 (> <) 时会产生严重错误!切记:比较和运算时,务必保证两边类型一致,或使用
$signed()强制转换。
总结
- 存储形式:补码(取反加一)。
- 核心优势:减法转加法,统一了加减法器,简化CPU设计。
- 数学边界:\(n\) 位有符号数范围是 \(-2^{n-1}\) ~ \(2^{n-1}-1\)(多了一个最小负数)。
- 硬件坑位:符号位扩展和有符号/无符号混合运算是RTL仿真中80%数值错误的根源,写代码时务必高度警惕。🚀
浙公网安备 33010602011771号