尧图精选

计算机的“算”到底怎么发生的?从万物皆加法说起

🕒 发布时间:2026/9/13 16:55:50 📁 来源:尧图网络
计算机的“算”到底怎么发生的从万物皆加法说起提到“计算机怎么算加减乘除”很多人第一反应是CPU里有个计算器输入两个数啪一下结果就出来了。再往深问一步两个数到底是怎么“啪”出来的市面上大多数教材的讲法是二进制、逻辑门、补码、加法器堆了一堆名词看完了还是不知道那几亿个晶体管里到底发生了什么。我今天想换个角度从一句话切入计算机里的一切运算本质上都是在做加法。减法是把减数变成补码再加回去乘法是加法的重复除法是减法的重复而减法又等价于加补码。所以绕了一大圈全部又回到加法上。理解了这句话计算机组成原理里最核心的那块拼图就算拼上了后面再看加法器、乘法器、浮点数精度这些题目都会顺很多。这篇文章适合三类人一是正在学计算机组成原理、数字电路被各种进位器和补码绕晕的学生二是写代码时对BigDecimal、浮点误差这些概念知其然不知其所以然的开发者三是纯粹好奇计算机内部工作机制、想真正搞明白“为什么它算得这么快”的爱好者。我会把原理讲透再把实操验证的方法也放进来了跟着做一遍要比单纯背概念管用得多。1. 为什么减法要用“补码”一次拿到答案的直观推理先从一个朴素的问题开始如果让你设计一套电路做减法你会怎么做最常见也最直接的做法是单独做一个减法器判断两个数的大小用大的减小的再根据结果符号位做处理。这个方案逻辑上完全成立但放到硬件里会带来现实问题电路要额外维护一套借位逻辑而且“比较大小”这件事本身也是一堆门电路才能完成面积大、延迟高。更麻烦的是计算机里用的是有限位数的二进制符号要额外用一位来表示一套电路既要处理符号又要处理数值复杂度会成倍上升。能不能让减法器直接复用加法器的电路这就是补码被发明出来的根本原因。先看一个十进制里的例子时钟上9点往前拨3个小时和往后拨9个小时结果是一样的。因为12小时制里9 - 3 9 9 (mod 12) 6。“往前拨3小时”等价于“往后拨9小时”这个9就是-3在模12下的补数。9 - 3直接变成了9 9减法消失了只剩加法。这个思想迁移到二进制里就是补码。假设我们用8位二进制存储整数能表示256个数0~255。我们希望x - y和x (y的补码)的结果一致而且是用的同一套加法电路不外扩任何额外逻辑。补码的求法很简单取反加一也就是先把y每一位取反再加1。为什么是取反加一而不是别的操作因为y (~y) 11111111再加1就变成了100000000在8位字长下这个最高位的1会被丢弃留在寄存器里的只是全0。也就是说y (~y 1) 0于是(~y 1)恰好就是-y。这样一来x - y x (~y 1)完全用加法表达了减法。还需要做一件事把符号位纳入同一套编码。最高位约定为符号位0表示正1表示负。正数直接原码表示负数则用其绝对值的补码表示。这样设计之后127和-128都能用8位表示整个可行域是-128到127比原本纯无符号0到255少了一个正的255但换来的是减法、加法、符号判定全部统一。我当年第一次接触补码时觉得“取反加一”像是提前背好的公式后来用时钟的例子理解一遍之后整个东西就变得很顺了。补码的本质就是让负数也遵循同一个加法器的物理规则是“硬件设计倒逼编码方式”的经典产物。如果你还想再直观验证一下可以自己手算几个例子8位补码下5 - 3变成00000101 11111101结果是00000010也就是2最高位的进位直接丢掉不影响结果3 - 5变成00000011 11111011结果是11111110也就是-2完全符合预期。2. 加法器的物理实现从串联进位到超前进位得到“全部用加法实现”的结论之后下一步自然是加法器这个电路到底怎么搭出来的这里没有必要害怕它的基础非常朴素控制好颗粒度就行。2.1 半加器和全加器加法电路的最小单元一位二进制加法有什么可能只有四种组合0000111011110。前三种都只输出一位第四种产生了一个进位。于是我们需要一个电路输入A和B输出和S以及进位C。这就是半加器逻辑很简单S是A和B的异或C是A和B的与。但真正的加法器不是孤零零处理一位的它必须考虑低一位传过来的进位因为11 01这样的算式最低位相加后会产生进位这个进位要参加到次高位。于是全加器登场了输入变成A、B、Cin来自低位的进位输出S和Cout。把n个全加器按位串联起来低位产生的进位接到高位的Cin上就是一个n位行波进位加法器Ripple Carry Adder。以4位加法器为例第0位先计算A0 B0 0结果S0输出Cout0流入第1位第1位计算A1 B1 Cout0依次类推最后一位产生的进位就是整个加法器的进位输出。这条链越长结果出来的越慢因为高位必须耐心等待低位的进位传递过来。2.2 串联进位的性能瓶颈串联进位的问题在8位、16位甚至32位加法器中会被放大每一位的S和Cout都是由输入到输出的组合逻辑延迟决定的n位链路的传播延迟大约正比于n。如果CPU的主频很激进一个加法周期才1ns结果进位链就要吃掉一半以上的时间剩下留给别的逻辑这显然不划算。所以在真实的CPU里加法器不会只靠串联实现而是引入了“超前进位”的思路让每一位的进位不依赖低位的实际结果而是直接用输入的各位自己算出“本位的进位是否可能产生”。每个位置根据A和B推断出一个Ggenerate是否本位置直接生成进位和一个Ppropagate是否能把进位继续传上去然后整个进位链可以并行计算出来不用一位位等。这是一种典型的面积换延迟思路硬件复杂度上去了但运算速度大幅提升。2.3 从电路到逻辑门的实操映射如果你手头有Logisim这类数字电路仿真软件完全可以自己搭一个4位加法器来验证先在左侧工具栏拖出异或门和与门做一个半加器再扩展为全加器用两个半加器加一个或门组成接着复制四份并依次连接进位输入输出。跑一次7 5观察对应位的灯第0位11出0进位1第1位101出0进位1第2位111出1进位1第3位001出1进位0最终结果1100正好是12。这个过程跑通之后你会发现所谓的“计算机计算”其实非常机械不是“理解”了75而是七个门电路在电压信号的驱动下顺着硬连线把结果推了出来。哪怕没有CPU、没有操作系统只给一堆的逻辑门和稳定的电源加法依然可以发生。提示如果仿真时出现进位信号不稳定先检查每一个全加器的进位输出是否连到了高位的进位输入这是我见过最多的连接错误。3. 乘法怎么变成一系列加法移位-加的递推逻辑加法搞定了乘法就相对轻松了因为乘法本来就可以拆解成重复的加法23 × 5 23 23 23 23 23。但真要按这个思路设计硬件性能会非常难看两个n位数相乘循环n次加法n小的时候无所谓一旦n变成32或者64几十上百次累加显然不适合作为CPU内部的基本操作。先看十进制手算竖式23 × 5 ----- 115分解来看就是(3×5) (20×5)15100115。二进制和十进制一样做竖式但规则还要更简单每一位的乘数只能是0或1所以每个部分积要么是被乘数本身要么是0。于是二进制的乘法可以总结为逐位扫描乘数如果当前位是1就把被乘数加到部分和上每处理一个更高位就把被乘数左移一位相当于乘2。这就是“移位-加”乘法器的核心原理。以5 × 3二进制0101 × 0011按4位计算为例乘数最低位是1部分和 被乘数0101处理下一位乘数第二位是1被乘数左移一位成1010部分和 1010-1111乘数第三位是0不累加第四位是0不累加结果1111即15。完整走一遍就明白其实每一轮干两件事判断乘数某一位是否为1把被乘数左移一位再决定加不加进部分和。硬件实现时用移位寄存器存乘数每轮移出最低位给控制逻辑另一个寄存器累加部分和最后结果寄存器保存累计值。乘法器本质上就是一个“循环加法器”控制逻辑负责判断何时加、何时只移位。这里有一个容易忽略的关键点判断当前位是1才加是0就不加为什么不直接把被乘数和乘数每一位都加进去因为如果乘数某一位是0那么它对最终结果没有任何贡献加进去只会白白浪费运算周期。所以判断这一步看起来简单实际是乘法器性能的重要优化点。现代的CPU乘法器还会做更进一步的空间换时间优化不是一位一位循环而是把多个部分积并行生成再用加法器树Wallace树/Dadda树一次压缩。这样原本n个周期的加法延迟可以被压缩到log₂n量级的门延迟。但无论结构怎么变底层逻辑仍然逃不开“部分积加法位移”只是把顺序执行改成并行压缩罢了。在Logisim里你可以自己做一个4位移位-加乘法器用一个4位寄存器存被乘数一个4位移位寄存器存乘数一个8位累加器存部分和。每来一个时钟上升沿检查乘数寄存器最低位为1就把被乘数加进累加器然后把被乘数左移一位、乘数右移一位。四个周期后累加寄存器里读到的就是乘积。亲测从搭电路到完整跑通大概一个小时绝对值回票价。4. 除法从减法到加法的“绕道”以及浮点数的另一层故事除法和乘法有点不对称乘法直接拆成加法和移位就能工作但除法的朴素想法是“重复减去除数”而减法我们用补码转换成加法所以除法本质上就是“反复加补码”的过程。4.1 恢复余数除法与非恢复余数除法以十进制长除法为原型二进制长除法要简单得多商的每一位要么是0要么是1判断标准就是“当前被除数余数够不够减去除数”。够减商落1减去除数不够商落0不减。硬件实现时常见的是恢复余数除法左移被除数/余数减去除数如果结果非负则商上1否则恢复余数并商上0。这个算法很直观但有个致命痛点——减完如果结果是负的还要把余数加回去恢复白白浪费了周期。于是有了不恢复余数除法SRT算法原理是如果减完结果是负说明不够减这时候不用恢复原余数而是直接把“余数左移一位再做一个加法运算”代替减法。为什么可以这样因为负余数左移一位后加上除数等价于“原余数左移一位再减去除数”这个比较过程的结果。用数学写就是设余数R 0先左移得2R再判断2R divisor 是否非负。这个过程不依赖恢复动作每轮固定一次操作计算速度大幅提升。不管哪种方案除法的计算量都明显大于乘法和加法所以CPU里除法的延迟往往是最高的。很多资料把除法器的逻辑看成“乘法的逆”确实有帮助但要理解它的实现细节还是得回到“减法转补码加法”这条路线上来。4.2 浮点数的除法、加减与精度陷阱进入浮点数小数之后加减乘除虽然名字上还是那些运算但底层遵循IEEE 754标准处理方式又多了一层。浮点数的本质是科学计数法的二进制版本V (-1)^sign × mantissa × 2^exponent所以浮点数加减法不能简单地把尾数二进制位直接丢进加法器而要先做“对阶”把指数较小的那个数尾数右移让两个数的指数一致然后尾数才能相加。对阶过程中右移出去的位会被舍入于是出现误差。这就引出一个很经典的工程问题0.1 0.2在JavaScript里输出0.30000000000000004。很多初学者认为是语言BUG其实不然。0.1和0.2的二进制展开都是无限循环小数任何有限位数的浮点数都无法精确表示它们运算结果自然也不是精确的0.3。这不是计算机“算错了”而是它忠实地计算了它所存的近似值。如果业务涉及金额、税率、科学计量这类对精度极度敏感的场景用二进制浮点数直接累加就会埋雷。常规做法是用十进制字符串/整数展开存储Java里用BigDecimal、Python里用decimal.Decimal、JavaScript里可以用专门的十进制运算库核心思路都是在小数点后不引入二进制近似而是用整数运算配合小数点位置来模拟十进制。我现在写任何涉及钱的逻辑时第一反应就是别用float/double哪怕只是做一次加法也不要偷懒。4.3 除法器的边界情况与溢出问题除法还有一个需要特别警惕的点溢出。整数除法中x / 0直接触发硬件异常浮点除法中除以0会得到Infinity或NaN这取决于操作数的符号和是否为零。很多年轻工程师写代码时拿到用户输入直接除没想过除数为0时的处理程序崩掉之后一脸茫然。实际中最好在计算前显式判断除数是否为零而不是依赖语言层面的兜底行为。另一个容易被忽略的是整数除法的截断方向。C语言和多数语言里-7 / 2 -37 / 2 3都是向零取整。但硬件的除法器默认做的是带符号整数除法对负数的处理本来就比正数要复杂所以一旦涉及负数除法的特殊取舍规则最好先在文档里查清楚再在软件层明确定义。5. 从“加法是基础运算”到真实场景里的工程实操看完前文的理论推导你可能已经理解“万物皆加法”这个说法的来源但距离实际工作还有一段路。真正写代码、做电路设计、处理数据时怎么把这个认知落地我结合自己踩过的一些坑讲几个具体场景。5.1 电路仿真实操在Logisim里验证“加法万能论”如果只想用软件验证一下“减法加补码”这件事我建议用Logisim搭一个4位补码加减法器准备4位输入A、4位输入B以及一位控制信号Sub当Sub0时不做额外处理直接把B送进加法器做AB当Sub1时用一个异或门数组把B的每一位取反然后通过加法器的一个低位进位输入把Cin置1相当于同时完成取反1最终输出就是A - B。这个方案在很多教材上叫“加减法一体电路”搭建起来不到10分钟却能非常直观地展示补码的妙处同一个加法器一个控制信号就能在加法和减法之间切换完全不需要第二套减法电路。搭完这个之后建议再把前文提到的移位-加乘法器试一下你会发现乘法器无非就是“重复使用这个一体加法器移位控制”整个计算机算术单元的逻辑是高度内聚的并不是一堆杂乱无章的门电路堆在一起。我一直觉得纸上谈兵一百遍不如仿真器里点一次鼠标。自己动手把这几个小电路跑通之后“万物皆加法”就不是一句口号而是你亲眼看到的事实。5.2 代码层面的精度处理与性能取舍作为开发者你可能更关心的是在实际程序里怎么用好这个“加法中心”原则。我总结几条经验金额计算必须用十进制定点方案。线上交易、薪资、税费之类场景直接用BigDecimal或decimal不要用float做中间量再转字符串假装没问题。以前有同事习惯用double累加再四舍五入一个月下来对不上账排查到凌晨才发现在某些小数点后几位上误差悄悄累积了。超大整数相加/相乘时可以用字符串模拟“手工加法”。这就是小学竖式运算的算法化逐位相加、记录进位最后把结果拼接出来。虽然时间复杂度是O(n)但n是位数不是数值大小对于超出64位范围的大数加法这是最直接的方案。我自己做数字签名相关的实验时就手工实现过这种“字符串大数加法”后面再换成更高效的库时心里也完全有底。浮点数比较要设阈值不等于直接比相等。因为0.10.2≠0.3本身就是预期行为直接比较大概率会出错。正确做法是两个数相减取绝对值小于一个极小的epsilon再视为相等。乘除法的性能代价远高于加减法。在高频循环或实时渲染里能通过预计算、查表、位运算替代乘除法就尽量替代。CPU虽然把加法做成了极速指令但乘法器延迟还是明显高于加法器除法更是最贵的所以编译器优化的很多技巧其实都是在减少乘除法次数。5.3 为什么“万物皆加法”在工程上很重要理解“计算机所有运算归根结底是加法”这件事真正的价值不在于炫技而在于帮助你建立正确的性能直觉。当你知道加法可以用几个门电路并行完成乘法需要若干轮移位-累加除法要对位数成倍执行恢复或非恢复循环你就会明白为什么有些运算那么快有些运算那么慢。写代码时一个随意的除法可能成为瓶颈一个精心设计的查表加法方案可能让吞吐量飙升——这不是玄学而是底层机制的外在表现。处理精度问题时知道浮点数的加法都要先做对阶和尾数舍入你就能理解为什么0.10.2不是精确结果。在设计数据库字段、接口返回、存储格式时你会更本能地去选择十进制定点数而不是二进制浮点数。这些都是花一小时理解补码加法器移位之后长期受益的回报。6. 回看整条链路从手动算数到CPU执行一条加法指令当你看到这里整条链路已经闭环了。我再把这套体系串一遍方便你回顾也算是我这些年反复给别人讲这个主题时沉淀下来的讲法。第一步二进制是一切编码的根基。有限数量物理状态最容易用0/1表示所以计算机选择了二进制而非十进制。第二步补码创造了“负数即正数的补”这一巧妙的编码方式让减法电路完全消失直接被合并进加法器这个设计节约了物理空间也统一了符号处理逻辑。第三步加法器由全加器逐位串联或超前进位构成最底层是少数几个逻辑门的组合。当我们说“CPU执行加法指令”时实际上就是程序计数器指向某条指令控制单元根据操作码打开相应通路让两个寄存器里的二进制数进入加法电路得到结果写回目标寄存器。整个过程耗电极少、延迟极短密集地重复数亿次/秒。第四步乘法器和除法器在加法器基础上增加移位寄存器与控制逻辑。乘法是“移位加法”的循环除法是“移位补码加法商值确定”的循环。第五步浮点数运算虽然多了一层指数对齐但底层的加法、比较、移位逻辑仍然沿用前四步搭建好的基础。我工作这几年越发觉得计算机很像一个极其刻板的算盘它不会“心算”不会“快速算法”只顽固而高效地执行加法。所有高级技巧包括乘法优化、除法SRT算法、超前进位都是为了让这个“只会加法的算盘”跑得再快一点、延迟再低一点。如果你刚接触计算机组成原理我的建议是别满足于“补码是取反加一”这种背诵结论花一个晚上搭一个真实的加法器和加减法器仿真电路你会获得比读十遍教材更深刻的理解。如果你是写代码的也建议花点时间把溢出、浮点精度、乘除法开销这三件事纳入日常编码习惯很多线上问题会提前被拦住。最后再分享一个我在Logisim实践里的小技巧搭建算术电路时复位清零和时钟节拍一定要提前设计好。很多学生搭好电路后发现结果不稳定其实不是因为逻辑错误而是寄存器没有在正确的时钟沿锁存数据。先画好数据流和控制信号时序再连线效率会高很多。
上一篇/下一篇内容由系统自动关联 返回资讯列表 →