ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

计算机如何实现加减乘除?从补码到加法器的原理与实践

计算机如何实现加减乘除?从补码到加法器的原理与实践 计算机是怎么把加减乘除跑起来的很多人第一次接触二进制加法器的时候会愣一下四则运算不是四个运算法则吗怎么全世界的教材都先教你加法后来我才想明白计算机本质上只懂一种运算——二进制加法。减法是加法的逆运算乘法和除法则是加法和移位的组合拳。这篇文章就围绕“计算机进行加减乘除的原理——万物皆加法”展开把补码、加法器、移位、溢出这些硬核概念掰开揉碎配合我实际调试中踩过的坑一起讲。适合刚学计算机组成原理的学生、准备计算机二级或考研的朋友也适合写代码时被数据溢出坑过的开发者。1. 为什么要说“万物皆加法”1.1 计算机只会一种运算二进制加法先把最底层的东西说清楚。计算机内部所有数据都是二进制电瓶高代表1低代表0。硬件层面做加法本质就是把两个二进制数逐位相加再加上低一位产生的进位。这玩意儿用逻辑门就能搭出来1 0 1结果位为1进位为01 1 0结果位为0进位为11 1 1 1结果位为1进位为1比如算一下 5 3。5 的二进制是 01013 的二进制是 0011从最低位开始加最低位1 1 0进位1第二位0 1 进位1 0进位1第三位1 0 进位1 0进位1最高位0 0 进位1 1进位0 结果是 1000也就是 8。整个计算过程只需要判断“当前位加进位”这件事逻辑门可以轻松实现这也是加法能成为所有运算地基的根本原因。1.2 整套算术体系如何被“加法”统一如果只有加法能跑那剩下的减法、乘法、除法去哪了关键思路就是转化减去一个数等于加上这个数的相反数。所以减法转成加法前提是能表示负数。乘法是重复的加法比如 5 × 3就是 5 5 5。硬件实现时通过“移位 加法”更高效但本质上还是加法。除法是重复的减法而减法又转成加法所以除法底层也是加法。被除数连续减去除数减到不够减为止数数减了多少次就是商剩下不够减的部分就是余数。你发现没有这一整套逻辑只需要三种“原料”二进制加法、左移右移、以及一种能表示负数的方案。补码负责解决最后一种然后整棵算术大树就全长出来了。1.3 为什么非要用加法硬件简单才是王道有同学会问直接在硬件上实现减法器、乘法器、除法器不可以吗做倒是能做但代价非常大。减法器需要处理借位逻辑乘法器直接硬算会需要大量全加器阵列除法器就更复杂了电路面积、功耗、设计难度都会成倍上升。想象一下你只要在口袋里放一把多功能瑞士军刀就能解决开瓶、剪线、拧螺丝所有问题。相比背上一个装满专用工具的箱子风险还更小、更好维护。计算机体系结构自冯·诺依曼结构提出以来一直追求“用最少、最可靠的硬件通过指令组合完成复杂任务”。把多种运算统一成加法是工程学的取舍结果电路复杂度低、时钟周期稳定、调试容易。所以“万物皆加法”不是一句文艺口号而是现代计算机性价比最高的实现路径。2. 减法变加法的关键补码2.1 先甩掉一个常识计算机里没有“减号”人类写算式时会写 10 - 3但计算机 CPU 里的运算器只有加法器和配套的取反电路。它不认识减号。为了把减法变成加法我们必须找到一种规则让“负数的二进制表示”参与加法时计算结果直接对。这里有个最基本的尝试直接用一个标志位表示正负。比如 8 位二进制里最高位当符号位0 表示正、1 表示负那 5 就是 0000 0101-5 就是 1000 0101。这样能不能直接用加法算 5 (-5)我们试一下0000 0101 1000 0101 1000 1010结果是 -10明显不对。更糟糕的是这种表示法里会有“正零”和“负零”两个零逻辑混乱。2.2 补码的由来模运算思想补码为什么能行它的思想源头是模运算。你可以想象一个只有 12 个刻度的钟表时间从 0 走到 11再走又从 0 开始。如果现在是 3 点想拨到 10 点可以正拨 7 个小时3 7 10也可以倒拨 5 个小时3 - 5 10。在模 12 的世界里-5 和 7 是等价的因为 7 ≡ -5 (mod 12)。计算机里的二进制数位数是固定的加法结果如果超出位数超出部分就被丢掉这就是“取模”。对于 n 位二进制数模就是 2ⁿ。所以只要找到一个“负数的补数”让负数参与加法时等效于模运算里的补数减法就能秒变加法。对于 n 位二进制数负数 -x 的补码定义就是 2ⁿ - x。例如 8 位二进制里模是 256-5 的补码就是 256 - 5 251写成二进制是 1111 1011。用这个数去加 50000 0101 1111 1011 1 0000 0000最高位进位超出 8 位被丢弃得到 0000 0000正好是 0。完美。2.3 补码的转化与计算实例实际操作中不需要真去减一次规则很简单正数补码就是原码本身负数补码等于“原码按位取反再加 1”也就是取反加一。举一个完整例子求 -3 的 8 位补码3 的二进制0000 0011按位取反1111 1100再加 11111 1101这就是 -3 的补码验证一下 5 (-3)5 的补码0000 0101-3 的补码1111 1101逐位相加0000 0101 1111 1101 1 0000 0010丢弃最高位进位得到 0000 0010即十进制 2结果完全正确。更妙的是符号位不再特殊它和数值位一样参与加法运算硬件根本不需要区分“这个位是不是符号”。溢出产生的进位要么丢弃要么进入专门的进位标志位由指令集层面去判断电路可以做得非常统一。下面列一张常见负数的补码对照表方便你快速心算十进制原码8位补码8位50000 01010000 0101-51000 01011111 101130000 00110000 0011-31000 00111111 1101-11000 00011111 1111-128不适用无符号位后溢1000 0000特别注意 -128 是特殊情况。8 位补码能表示的范围是 -128 到 127-128 的补码 1000 0000它的原码和反码都没有有效对应。这也是“补码负数范围比正数多一个”的原因考试和面试里经常出这个点。2.4 溢出与标志位运算器如何感知出错补码虽然好用但它有个绕不开的坑溢出。两个正数相加结果变成负数两个负数相加结果变成正数都是溢出。看一个经典的例子8 位补码里 100 50100 的补码0110 010050 的补码0011 0010相加结果1001 01101001 0110 在补码规则下对应的是 -106而正确结果应该是 150。怎么处理硬件上有两个状态位来帮忙进位标志CF和溢出标志OF。CF 记录最高位有没有产生进位无符号运算用OF 记录“符号位进位”和“最高数值位进位”是否不一致有符号运算用。判断方式很简单正数加正数结果最高位变 1或者负数加负数结果最高位变 0就是溢出。我这里想提醒一下很多刚开始看教科书的人分不清 CF 和 OF。打个比方如果你把 8 位二进制当成“不带符号的 0 到 255”那进位就是 CF如果把它当成“带符号的 -128 到 127”那结果跑出这个范围就是 OF。一个管长度一个管符号方向别混。3. 乘法与除法加法与移位的组合拳3.1 手工竖式的二进制版本小学算乘法时我们用竖式第一个数乘以第二个数的每一位然后按位对齐相加。二进制乘法一模一样而且更简单因为每一位只能是 0 或 1乘以 0 得 0乘以 1 得原数。你甚至不用背九九乘法表。举例计算 13 × 11二进制分别是 1101 和 10111101 × 1011 --------- 1101 (被乘数 × 1) 1101 (被乘数 × 1左移 1 位) 0000 (被乘数 × 0左移 2 位) 1101 (被乘数 × 1左移 3 位) --------- 1000111110001111 转十进制是 143正好等于 13 × 11。整个过程就两件事判断乘数当前位是 1 还是 0如果是 1 就把被乘数加到结果上然后把被乘数左移一位。左移在硬件里就是连线和移位寄存器的活成本极低。3.2 乘法器的硬件实现流程用程序逻辑描述二进制乘法器核心算法是初始化乘积为 0。从乘数最低位开始逐位检查。当前位为 1则把“被乘数左移后的副本”加到乘积当前位为 0则只做移位不加任何数。每处理完一位被乘数左移一位乘数右移一位准备看下一位。重复 n 次n 是二进制位数。现代处理器里的乘法器早就做了各种优化不再一位一位慢慢来。它会用 Booth 编码减少相加次数或者用华莱士树把多个部分积并行压缩。但不管优化多花哨最底层的那一叠“部分积”还是要靠加法器垒出来。所以说乘法器是“加速过的加法阵列”没毛病。3.3 除法器减法循环与恢复余数法除法比乘法麻烦因为它不只有一个结果还带余数。硬件除法最朴素的做法是“循环试减”被除数减去除数减一次商加一直到不够减为止。这在高位大数字场景下非常慢极端情况下 2³² 除以 1 要减几十亿次。为了提速工程上常用恢复余数法。它的核心思想是二进制长除法将被除数逐位移入余数寄存器。每次移动一位后用“余数 被除数当前位”去减去除数。够减商位写 1不够减商位写 0并需要把余数恢复回去这就是“恢复”二字的来源。反复 n 次寄存器里剩下的是商和余数。看起来复杂本质仍然是加法减法是补码加法和移位的组合。这也是“万物皆加法”最彻底的地方除法不是简单重复减几次而是利用位运算把复杂度压到 O(n) 轮每轮只做一次加减和一次移位。这里有个我自己踩过的坑写模拟除法器的时候恢复余数的时机非常关键。如果你在“不够减”之后没有及时恢复余数下一轮移位出来的数就是错位的最终结果会差之千里。调试这类问题最好的办法是把每一轮的“余数、商、被除数”打印出来逐行对照手算草稿一眼就能看出是第几位出错。3.4 现代处理器的优化方向真实的 CPU 里乘除法不是简单的黑盒指令周期差异很大。加法指令可能只需要 1 个时钟周期乘法指令大约 3 到 5 个周期除法指令可能高达 20 到 90 个周期。原因正如前面所说除法天然需要多次迭代。为了减少这个差距现代处理器一般会做几件事乘法和除法器拆成独立的执行单元不占用普通 ALU 流水线。通过指令级并行硬件能同时处理多个运算掩盖除法延迟。编译器层面除以一个常数会被优化成“乘以倒数”或“移位组合”因为硬件乘法比除法快得多。你在写代码时也能利用这个知识如果循环里频繁除以 2 的幂最好直接写成右移如果必须做浮点乘除思考能不能通过整数运算等价替代。把底层逻辑摸透了优化方向自然就清楚了。4. 从原理走向实践开发中遇到的加法陷阱4.1 整数溢出最经典的补码坑理解了补码以后看很多开发问题就像看白纸上的黑点。最典型的就是整数溢出。以前我在一个数据统计模块里用 32 位有符号整数记录计数值数据量到 21 亿左右的时候突然变成负数。排查半天发现是累计值跨过 2³¹ - 1最高位变 1补码解释为负数。这个问题的本质就是加法器不会主动告诉你结果超出了类型范围它只是老老实实算出一个二进制序列由类型规则去解释成负数。修复的方法很多换 64 位类型或者每次累加前加上界判断。另一个容易忽略的场景是无符号整数。C/C 里的 unsigned int 做减法如果被减数小于减数结果会变成一个巨大的正数。这其实也是补码在起作用负数被解释成了无符号范围下的“正数”。很多网络协议里的序号计算就是用无符号整数做差来判断先后顺序一旦没注意精度就会产生离奇的 bug。4.2 浮点数的精度问题与BigDecimal聊到“加减乘除”浮点数是躲不开的坎。很多人第一次被 $0.1 0.2$ 不等于 0.3 惊到就是因为没理解浮点数的二进制表示。IEEE 754 浮点数用“符号位 指数位 尾数位”记录数据指数位做移位尾数位类似科学计数法的小数部分。问题来了十进制的 0.1在二进制里是无限循环小数存储时只能截断。结果就是 0.1 0.2 得到 0.30000000000000004这是二进制加法在十进制重解释下的“残余误差”。如果做金融、计费类的项目正确姿势是使用 BigDecimal 或定点数。BigDecimal 的核心就是把小数拆成整数和精度信息用整数运算完成底层加减乘除绕开二进制的浮点表示。使用 BigDecimal 时有个经典坑不要用new BigDecimal(0.1)因为传入 double 0.1 时已经带上了浮点误差。正确做法是用new BigDecimal(0.1)从字符串构造才能保证精度。看起来是多写一个引号的事背后其实是补码和浮点知识在指导工程决策。4.3 位运算优化用移位代替乘除法既然乘法除法底层就是移位加加法那反过来很多乘除运算也能用移位完成。这是优化代码时非常好用的思路x * 2可以写成x 1。x / 2可以写成x 1但注意只对正数安全。x * 10可以拆成(x 3) (x 1)因为 10 8 2。判断奇偶用x 1比x % 2效率略高。我在做嵌入式开发时曾把一个图像处理函数里的乘法全部改成移位和加法组合运行时间缩短了接近一倍。这种优化不挑语言C、Java、JavaScript 都能用。但要小心现代编译器和 JIT 已经非常聪明普通的乘除法它自己就能优化成移位指令你手动写的移位反而可能降低可读性。所以这个技巧最适合的场景是嵌入式底层代码、算法框架里的热循环、以及对编译器不信任的极端性能优化。4.4 常见问题速查表我按自己的踩坑经历整理了一张速查表希望对你有帮助现象底层原因处理方案正数累加最后变成负数有符号补码溢出换更大整数类型或加溢出判断无符号整数相减得到巨大数值补码负数被解释为无符号数先比较大小再决定运算顺序0.1 0.2 ≠ 0.3二进制无法精确表示十进制小数用 BigDecimal 或定点数除以 2 的幂不够快除法指令周期长改用右移或编译器优化负数右移结果不符合预期算术右移补符号位明确使用逻辑右移或转换类型这张表里的每一行背后都对应着补码、加法器、移位等基础原理。遇到类似问题不再需要死记结论你只要想一遍“数在机器里到底是怎么存的、加法器算完以后哪位溢出了”基本就能猜出问题出在哪里。我在实际工作中最大的体会是计算机的“万物皆加法”不是一句冷冰冰的理论它是一把实用的排查工具。每当你遇到数字相关的 bug都可以追溯到这条链条——数的表示补码→ 运算的机制加法器→ 类型与精度的解释编程语言。把这三个环节想通很多问题看一眼就能定位。最后分享一个小技巧想验证自己对这部分内容的理解别只看书去手写一个 8 位二进制加减法器模拟器用代码实现“取反加一”、逐位进位、溢出判断。写完以后再回头读计算机组成原理里“运算器”那一章你会发现自己像是翻开了新书。这也是我当年把加减乘除彻底搞懂的最快路径。
返回列表