1. 从算盘到芯片:为什么每个开发者都该补上这堂历史课
很多人学计算机组成原理,一上来就被各种门电路、寄存器、总线时序图砸得头晕,觉得这些老古董跟日常写代码八竿子打不着。我刚开始带新人的时候也发现,大部分人能熟练使用框架、调库、写业务逻辑,但一旦问起“为什么内存访问比硬盘快那么多”“为什么32位系统最多只能认到4GB内存”“为什么浮点数运算有时候结果会莫名其妙不对”,就答不上来了。这些问题的答案,其实都藏在计算机的发展史里。
计算机的发展史不是让你背年份和人物名字的。它真正有价值的地方在于:每一代计算机的演进,都是因为遇到了一个具体的瓶颈,然后有人提出了一个巧妙的解决方案,这个方案又带来了新的瓶颈,如此循环往复。理解了这条“瓶颈-突破-新瓶颈”的链条,你对计算机体系结构的理解就不再是零散的知识点,而是一条有因果关系的逻辑线。
这篇文章适合谁看?如果你是计算机相关专业的学生,正在学组成原理但觉得历史部分枯燥无味,那我会用从业者的视角帮你把这段历史和后面的技术原理串起来。如果你已经工作了,想补一补底层知识,那更好,因为你带着实际问题来学,理解会更深。如果你纯粹是对计算机怎么从几十吨重的庞然大物变成你手里这块芯片感兴趣,那也完全没问题,我会尽量用生活化的类比把原理讲清楚。
接下来的内容,我会按照计算机发展的几个关键阶段来展开,每个阶段重点讲清楚三件事:当时遇到了什么问题、解决方案的核心思路是什么、这个方案对后来的计算机体系结构产生了什么影响。同时我会穿插一些实操层面的内容,比如怎么用简单的代码验证不同架构的差异、怎么理解存储层次的设计逻辑。这些不是教科书上的标准内容,而是我在实际工作中踩过坑之后总结出来的理解方式。
2. 计算机发展史的四段式拆解与核心逻辑
2.1 为什么按“器件”来划分世代最合理
计算机发展史有很多种划分方式,按年代分、按应用领域分、按体系结构分都有。但最主流的还是按核心器件的演变来划分世代,原因很简单:器件决定了计算机的速度、体积、功耗和可靠性,而这些指标又直接决定了计算机能用来做什么。你可以把器件想象成汽车的发动机,发动机的技术水平决定了这辆车能跑多快、能拉多少货、油耗多少。
第一代是电子管时代,时间大概在20世纪40年代到50年代中期。电子管这东西本质上就是一个玻璃壳里封了电极,通电之后电子在真空中从阴极飞向阳极。用它来做开关,速度比机械继电器快得多,但问题是体积大、发热严重、寿命短。一台机器用上万个电子管,平均每几个小时就有一个管子烧掉,维护人员得不停地换管子。这个时代的代表机器是ENIAC,占地170平方米,重30吨,功耗150千瓦。你拿它跟现在的手机比,算力可能还不如一个计算器,但在当时已经是革命性的突破了。
第二代是晶体管时代,大概从50年代中期到60年代中期。晶体管用半导体材料替代了真空管,体积缩小到原来的几十分之一,功耗大幅降低,寿命也长得多。这个变化带来的直接后果是计算机不再只能待在恒温恒湿的机房里,开始进入一些企业的办公环境。晶体管计算机的运算速度从每秒几千次提升到了每秒几十万次。
第三代是集成电路时代,从60年代中期到70年代初。集成电路的思路很简单:既然晶体管可以做得那么小,那为什么不把多个晶体管连同电阻、电容一起做在同一块半导体材料上?这一下就把计算机的核心部件从“一堆独立元件焊接在一起”变成了“一块芯片上集成了成百上千个元件”。可靠性大幅提升,因为焊点少了,接触不良的概率就低了。速度也上去了,因为元件之间的连线变短了,信号传输延迟减小了。
第四代是大规模集成电路时代,从70年代初一直延续到现在。集成电路的集成度从最初的小规模(几十个元件)发展到中规模(几百个)、大规模(几千个)、超大规模(几万到几百万个),再到现在的极大规模(几十亿个晶体管在一块芯片上)。这个阶段最重要的变化是微处理器出现了,把整个CPU做在一块芯片上,计算机的体积和成本急剧下降,个人计算机才成为可能。
注意:世代划分不是一刀切的,过渡期往往有好几年,不同来源的划分标准也略有差异。考试或者写报告的时候,关键是把每个世代的核心特征和技术突破讲清楚,具体年份不用死记。
2.2 冯·诺依曼结构到底解决了什么问题
讲计算机发展史,冯·诺依曼结构是绕不过去的。但很多人只是背了“存储程序”这四个字,并不理解它为什么重要。我来还原一下当时的场景。
早期的计算机,比如ENIAC,编程方式是这样的:你要算一个题目,得手动插拔电缆、拨动开关来设置机器的工作方式。换一个题目,就得重新接线。这就像你家里的电器,每换一个功能就得重新拆开改电路,效率极低。ENIAC的编程团队里有一批人专门负责接线,她们的工作强度非常大,而且容易出错。
冯·诺依曼提出的方案核心就一句话:把程序也当成数据,存在存储器里。计算机从存储器里取指令、取数据,按照指令的要求做运算,再把结果存回存储器。这样一来,换程序就像换一个文件一样简单,不需要动硬件。这个思路在今天看来理所当然,但在当时是颠覆性的。
冯·诺依曼结构的五大部件是:运算器、控制器、存储器、输入设备、输出设备。运算器和控制器合在一起就是CPU。这个结构的关键特征是“指令和数据放在同一个存储器里,用同样的方式访问”。这带来了一个著名的瓶颈,叫冯·诺依曼瓶颈:CPU和存储器之间的数据传输速率限制了整个系统的性能。因为指令和数据共享一条总线,不能同时取指令和取数据,所以CPU再快,也得等存储器把数据送过来。
这个瓶颈直接催生了后来的缓存(Cache)设计。缓存的思路是在CPU和主存之间加一层小而快的存储器,把最近用到的指令和数据放进去,CPU大部分时间从缓存里取数据,就不用等慢速的主存了。你写代码的时候可能没感觉,但缓存命中率对程序性能的影响非常大。我做过一个测试,同样的算法逻辑,只是把二维数组的遍历顺序从行优先改成列优先,在数据量大的时候性能差了好几倍,原因就是缓存命中率不同。
2.3 摩尔定律为什么能持续这么久
摩尔定律大家应该都听过:集成电路上可容纳的晶体管数目,约每隔两年便会增加一倍。但很少有人想过,为什么偏偏是两年翻一倍,而不是一年或者五年?这背后其实是一个经济学和物理学共同作用的结果。
从物理学角度讲,晶体管越小,开关速度越快,功耗越低。但缩小是有极限的,当晶体管的尺寸小到几十纳米的时候,量子隧穿效应就变得显著了,电子会直接穿过绝缘层,导致漏电流增加。这就是为什么近几年制程工艺的进步速度明显放缓了。
从经济学角度讲,建一条先进的晶圆生产线动辄需要上百亿美元,只有产量足够大才能摊薄成本。如果晶体管数量翻倍的速度太快,市场消化不了这么多芯片,厂商就收不回投资。如果太慢,又会被竞争对手超越。两年翻一倍是一个经过市场验证的、可持续的节奏。
摩尔定律对计算机体系结构的影响是深远的。它意味着每隔几年,同样面积的芯片上就能集成更多的功能。这直接推动了从单核到多核的转变。当单核频率提升遇到功耗墙的时候,厂商不再追求更高的主频,而是把多个核心做在同一块芯片上。你写代码的时候如果还是单线程思维,那就浪费了多核的算力。我见过不少项目,性能瓶颈明明在CPU上,但代码是单线程的,加一个线程池就能解决的问题,硬是拖了几个月。
2.4 存储层次结构的演进逻辑
存储层次结构是计算机组成原理里最实用的一个知识点,但很多教材讲得太抽象。我用一个生活类比来解释:你办公桌上放的是正在处理的文件,这是寄存器;抽屉里放的是今天可能要用的资料,这是缓存;书架上放的是这个月要用的参考书,这是主存;档案室里放的是去年的项目文档,这是硬盘。越靠近你的存储越快越小越贵,越远端的存储越慢越大越便宜。
这个层次结构不是一开始就有的,而是随着计算机发展逐步形成的。早期计算机只有主存和硬盘两级,CPU直接访问主存。后来CPU速度越来越快,主存速度跟不上,就加了缓存。再后来缓存也不够快了,又分成了L1、L2、L3多级缓存。现在的高性能处理器,L1缓存只有几十KB,但访问延迟只有几个时钟周期;L3缓存有几MB到几十MB,延迟几十个时钟周期;主存有几GB到几十GB,延迟几百个时钟周期;硬盘有几TB,延迟几百万个时钟周期。
理解这个层次结构对你写代码有直接的指导意义。比如为什么数据库的索引要用B+树而不是二叉树?因为B+树的节点大小可以设成跟磁盘块一样大,一次磁盘IO就能读入一个节点,减少了磁盘访问次数。为什么遍历大数组的时候要尽量顺序访问?因为顺序访问的缓存命中率高,随机访问会导致缓存频繁失效。这些优化技巧的背后,都是存储层次结构在起作用。
3. 从电子管到微处理器的关键技术跃迁
3.1 电子管时代的工程极限与突破
电子管计算机的工程实现,用今天的眼光看简直是行为艺术。ENIAC用了17468个电子管,7200个晶体二极管,1500个继电器,70000个电阻,10000个电容。这些元件焊接在成千上万的接线上,整台机器重达30吨。为了保证电子管正常工作,机房需要恒温恒湿,而且要有专门的维护团队24小时轮班,随时准备更换烧掉的管子。
这个时代的计算机编程方式也很有意思。ENIAC的编程是通过设置开关和插拔电缆来实现的,相当于用硬件连线来写程序。后来出现了穿孔卡片和纸带,程序被编码成一排排的孔洞,机器通过光电传感器读取。这种方式虽然比接线进步了,但写程序还是很麻烦,改一个指令就得重新打一张卡片。
电子管时代最大的技术贡献是确立了计算机的基本架构。虽然ENIAC本身不是存储程序式的,但冯·诺依曼在参与ENIAC项目之后提出了存储程序的概念,并在EDVAC的设计中实现了。这个架构一直沿用至今,你手机里的芯片本质上还是冯·诺依曼结构。
实操心得:如果你在学组成原理的时候觉得冯·诺依曼结构太抽象,可以试着用Python写一个简单的指令集模拟器。定义几条基本指令(加载、存储、加法、跳转),用一个列表模拟存储器,用一个循环模拟CPU的取指-译码-执行过程。写完之后你对存储程序的理解会深刻得多。
3.2 晶体管如何让计算机走出机房
晶体管替代电子管,带来的变化不只是体积和功耗。更重要的是可靠性的大幅提升。电子管的平均无故障时间只有几千小时,而晶体管的寿命可以到几十万小时。这意味着计算机不再需要时刻有人守着换管子,可以连续运行很长时间。这个变化让计算机从实验室走向了商业应用。
第一台晶体管计算机是TRADIC,1954年由贝尔实验室研制。它用了不到800个晶体管,功耗只有100瓦,体积只有一立方米左右。跟ENIAC比起来,简直是天壤之别。但TRADIC的运算速度并不比ENIAC快多少,因为它的设计目标不是追求速度,而是验证晶体管在计算机中的可行性。
晶体管计算机的真正爆发是在50年代末到60年代初。IBM的7090系列是当时最成功的晶体管计算机,运算速度达到每秒几十万次,被广泛用于科学计算和工程仿真。这个时期的计算机开始有了操作系统的雏形,出现了批处理系统,可以把多个任务排队,由计算机自动依次执行,不需要人工干预。
从体系结构的角度看,晶体管时代最重要的变化是变址寄存器和浮点运算单元的出现。变址寄存器让程序可以方便地访问数组和数据结构,浮点运算单元让计算机可以处理实数运算。这两个特性极大地扩展了计算机的应用范围,从纯数值计算扩展到了数据处理和工程计算。
3.3 集成电路带来的可靠性革命
集成电路的发明是计算机发展史上的一个转折点。在集成电路之前,计算机的制造方式是:先生产出独立的晶体管、电阻、电容,然后把它们焊接在电路板上。一台计算机有成千上万个焊点,任何一个焊点出问题都会导致故障。焊点的可靠性是当时计算机维护的最大难题。
集成电路把多个元件做在同一块半导体衬底上,元件之间的连接通过芯片内部的金属布线实现,不需要焊接。这一下就把焊点数量减少了好几个数量级。可靠性提升的同时,成本也大幅下降,因为芯片可以批量生产,每块芯片的边际成本很低。
集成电路的集成度提升遵循摩尔定律,从SSI(小规模集成,几十个元件)到MSI(中规模,几百个)、LSI(大规模,几千个)、VLSI(超大规模,几万到几百万个)。每提升一个级别,计算机的性能和功能就上一个台阶。
这个时期出现了微程序控制的概念。微程序控制把一条机器指令分解成多个微操作,每个微操作对应一个微指令,微指令存储在控制存储器里。执行一条机器指令就是依次执行一系列微指令。这个设计让控制器的设计变得规整,容易修改和扩展。你如果学过计算机组成原理,里面的微指令和微操作概念就是从这个时期来的。
3.4 微处理器如何把计算机装进口袋
微处理器的出现是计算机发展史上最具革命性的事件之一。在微处理器之前,CPU是由多个芯片组成的,运算器一块芯片、控制器一块芯片、寄存器一组芯片。微处理器把整个CPU做在一块芯片上,体积和成本都大幅下降。
第一块微处理器是Intel 4004,1971年推出。它最初是为计算器设计的,只有4位字长,集成度2300个晶体管。但它的出现证明了一件事:通用处理器可以做得这么小、这么便宜。这直接催生了个人计算机产业。
微处理器的发展速度非常快。从4位到8位到16位到32位到64位,从单核到多核,从几MHz到几GHz。你手机里的处理器,性能已经超过了十几年前的大型机。这个进步的背后,是半导体制造工艺的持续进步和体系结构设计的不断创新。
从体系结构的角度看,微处理器时代有几个重要的技术节点。流水线技术让多条指令可以重叠执行,提高了指令吞吐率。超标量技术让处理器可以在一个时钟周期内发射多条指令。乱序执行技术让处理器可以根据数据依赖关系动态调整指令执行顺序,减少流水线停顿。分支预测技术让处理器可以提前猜测分支方向,避免等待分支结果。这些技术你不需要全部深入掌握,但理解它们的基本思路,对你写出高性能代码是有帮助的。
常见问题:为什么我的程序在单核上跑得好好的,放到多核上反而变慢了?这通常是因为多线程之间的同步开销超过了并行带来的收益。线程的创建、销毁、上下文切换、锁竞争都需要时间。如果任务本身很小,或者线程之间需要频繁共享数据,多线程反而会拖慢速度。解决办法是减少线程数量、使用无锁数据结构、或者把任务粒度调大。
4. 实操验证:用代码和工具理解架构演进
4.1 用Python模拟指令执行流程
理解冯·诺依曼结构最好的方式是自己写一个简单的模拟器。下面这段代码实现了一个极简的CPU,支持加载、存储、加法、跳转、停机五条指令。你可以直接运行,观察指令和数据如何在存储器中交互。
class SimpleCPU: def __init__(self, memory_size=256): self.memory = [0] * memory_size self.pc = 0 # 程序计数器 self.acc = 0 # 累加器 self.running = True def load_program(self, program, start=0): for i, instr in enumerate(program): self.memory[start + i] = instr def run(self): while self.running: instr = self.memory[self.pc] opcode = instr >> 4 operand = instr & 0x0F self.pc += 1 if opcode == 0x1: # LOAD self.acc = self.memory[operand] elif opcode == 0x2: # STORE self.memory[operand] = self.acc elif opcode == 0x3: # ADD self.acc += self.memory[operand] elif opcode == 0x4: # JMP self.pc = operand elif opcode == 0xF: # HALT self.running = False print(f"PC={self.pc-1:02d} ACC={self.acc:3d} MEM={self.memory[:8]}") # 程序:把地址10和11的值相加,结果存到地址12 program = [ 0x1A, # LOAD 地址10 0x3B, # ADD 地址11 0x2C, # STORE 地址12 0xF0, # HALT ] cpu = SimpleCPU() cpu.memory[10] = 7 cpu.memory[11] = 5 cpu.load_program(program) cpu.run()运行这段代码,你会看到每一步执行后累加器和存储器的变化。这个模拟器虽然简单,但包含了冯·诺依曼结构的核心要素:程序存储在存储器中、指令顺序执行、指令和数据用同样的方式访问。你可以试着修改程序,加入循环和条件跳转,感受一下程序是如何控制硬件行为的。
4.2 用命令行工具观察缓存对性能的影响
缓存对性能的影响,用一个小实验就能直观感受到。下面这段C代码创建了一个大数组,然后分别按行优先和列优先两种方式遍历。编译运行之后对比耗时,你会看到明显的差异。
#include <stdio.h> #include <stdlib.h> #include <time.h> #define N 4096 int main() { int *matrix = (int *)malloc(N * N * sizeof(int)); if (!matrix) return 1; // 初始化 for (int i = 0; i < N * N; i++) { matrix[i] = i; } clock_t start, end; volatile long sum = 0; // 行优先遍历 start = clock(); for (int i = 0; i < N; i++) { for (int j = 0; j < N; j++) { sum += matrix[i * N + j]; } } end = clock(); printf("行优先耗时: %f 秒\n", (double)(end - start) / CLOCKS_PER_SEC); // 列优先遍历 start = clock(); for (int j = 0; j < N; j++) { for (int i = 0; i < N; i++) { sum += matrix[i * N + j]; } } end = clock(); printf("列优先耗时: %f 秒\n", (double)(end - start) / CLOCKS_PER_SEC); free(matrix); return 0; }在大多数机器上,列优先遍历的耗时会明显高于行优先,差距可能在几倍到十几倍之间。原因就是缓存的空间局部性原理:行优先遍历时,相邻的元素在内存中也是相邻的,一次缓存行加载可以服务多次访问;列优先遍历时,每次访问的元素在内存中相隔N个位置,缓存行加载之后大部分数据都用不上,缓存命中率极低。
这个实验不需要任何特殊工具,用系统自带的编译器就能做。我建议你在自己的机器上跑一遍,亲眼看到差异之后,你对缓存的理解会比看十遍书都深刻。
4.3 用系统信息工具查看存储层次
如果你想了解自己机器的存储层次结构,可以用一些系统自带的工具来查看。在Linux系统上,lscpu命令可以显示CPU的缓存大小和核心数。free -h可以查看内存大小和使用情况。lsblk可以查看磁盘设备。
# 查看CPU缓存信息 lscpu | grep -i cache # 查看内存信息 free -h # 查看磁盘信息 lsblk -o NAME,SIZE,TYPE,MOUNTPOINT在Windows系统上,可以用任务管理器的性能标签页查看CPU、内存、磁盘的实时使用情况。也可以用wmic命令获取更详细的信息。
# Windows下查看CPU缓存 wmic cpu get L2CacheSize, L3CacheSize # 查看内存 wmic memorychip get Capacity, Speed这些工具输出的数字,跟你在组成原理课上学到的存储层次结构是一一对应的。看到实际数字之后,你会对“寄存器最快最小、硬盘最慢最大”这个规律有更具体的感受。
实操心得:我建议你在自己的开发机上跑一下上面这些命令,把结果记下来。以后遇到性能问题的时候,先看看数据量跟缓存大小的关系,很多时候问题就定位了。比如你处理一个100MB的数据集,而L3缓存只有8MB,那就要考虑分块处理,每次只加载能放进缓存的数据块。
5. 常见问题与排查技巧实录
5.1 为什么32位系统只能认到4GB内存
这个问题在组成原理的考试和面试里出现的频率很高,但很多人只是背了答案,不理解背后的计算过程。我来完整推导一遍。
32位系统的地址总线宽度是32位,意味着CPU能产生的地址范围是0到2的32次方减1。2的32次方等于4294967296,也就是4GB。每个地址对应一个字节的存储单元,所以最大能访问4GB的内存空间。
但实际可用内存往往不到4GB,因为有一部分地址空间要留给硬件设备。比如显卡的显存、网卡的控制寄存器、BIOS的ROM等,都需要映射到地址空间中。这些设备占用的地址范围,内存就不能再用了。所以你在32位系统上看到的内存容量可能是3.2GB、3.5GB,具体取决于硬件配置。
64位系统的地址总线宽度是64位,理论寻址范围是2的64次方,这个数字大到没有实际意义。目前主流的64位处理器实际使用的地址线是40到48位,对应的寻址范围是1TB到256TB,足够用很多年了。
注意:这里说的32位和64位指的是处理器的通用寄存器宽度和地址总线宽度。有些处理器是32位寄存器但支持36位地址总线,可以通过物理地址扩展技术访问超过4GB的内存,但单个进程的虚拟地址空间仍然受限于32位。
5.2 浮点数运算为什么会有精度问题
浮点数精度问题是另一个高频疑问。很多人写代码的时候发现0.1加0.2不等于0.3,觉得计算机是不是算错了。其实计算机没算错,是浮点数的表示方式决定的。
计算机用二进制表示浮点数,遵循IEEE 754标准。一个浮点数由符号位、指数位和尾数位组成。问题在于,很多十进制小数在二进制下是无限循环小数。比如0.1在二进制下是0.00011001100110011...无限循环。计算机只能存储有限位数的尾数,所以0.1在计算机里存储的是一个近似值。两个近似值相加,误差就显现出来了。
这个问题的解决办法是:在需要精确计算的场景(比如金融计算),不要用浮点数,用整数或者定点数。比如金额可以用“分”作为单位存整数,计算完了再转成“元”显示。如果必须用浮点数,那就控制比较的精度,不要用等号直接比较,而是判断两个数的差值是否小于一个很小的阈值。
# 错误的比较方式 print(0.1 + 0.2 == 0.3) # 输出 False # 正确的比较方式 epsilon = 1e-9 print(abs((0.1 + 0.2) - 0.3) < epsilon) # 输出 True5.3 程序性能瓶颈的快速定位思路
当你觉得程序跑得慢的时候,不要盲目优化。先定位瓶颈在哪里,再针对性地解决。下面这个排查顺序是我在实际工作中总结出来的,从快到慢、从简单到复杂。
| 排查步骤 | 检查内容 | 常用工具 | 典型问题 |
|---|---|---|---|
| 1 | CPU使用率 | top, 任务管理器 | 单线程跑满一核,其他核空闲 |
| 2 | 内存使用 | free, 任务管理器 | 内存泄漏,频繁GC |
| 3 | 磁盘IO | iostat, 资源监视器 | 频繁读写小文件 |
| 4 | 网络延迟 | ping, traceroute | 跨区域调用,DNS解析慢 |
| 5 | 缓存命中 | perf, VTune | 随机访问大数组 |
| 6 | 锁竞争 | 线程分析工具 | 多线程抢同一把锁 |
大部分性能问题在前三步就能定位。我见过很多项目,性能瓶颈其实是数据库查询没有加索引,导致全表扫描,磁盘IO爆了。这种情况下优化代码逻辑没用,加个索引就解决了。
避坑技巧:不要凭直觉猜瓶颈。我见过一个团队花了两个月优化算法,最后发现瓶颈在日志打印上,每次请求都同步写磁盘日志,把IO拖垮了。先用工具测量,再动手优化。
5.4 学习组成原理的常见误区
最后说几个学习组成原理时容易踩的坑。第一个误区是死记硬背。组成原理的知识点之间是有逻辑关系的,理解了“为什么”比记住“是什么”重要得多。比如你理解了冯·诺依曼瓶颈,自然就能理解为什么要有缓存,为什么要有存储层次结构。
第二个误区是只看书不实践。组成原理是一门很具体的课,很多东西在纸上推一遍不如在机器上跑一遍。我前面给的模拟器代码和缓存实验,花半个小时跑一下,比看一天书都有用。
第三个误区是觉得历史部分没用。很多人觉得发展史就是背年份,考试完就忘了。但如果你把发展史理解成“问题-方案-新问题”的演进链条,它其实是整门课的索引。每个技术点都能在历史中找到它要解决的问题,这样学起来就有方向感了。
第四个误区是只学不总结。组成原理的知识点很碎,寄存器、总线、中断、DMA、流水线、缓存、虚拟内存,每一个都能展开讲很多。学完一个阶段之后,试着用自己的话把核心逻辑讲一遍,或者画一张思维导图,把知识点之间的关联理清楚。能讲清楚,才说明真懂了。
我在实际带新人的过程中发现,那些组成原理学得扎实的人,排查问题的速度明显更快。因为他们知道数据在计算机里是怎么流动的,遇到性能问题能快速定位到是CPU、内存、磁盘还是网络的瓶颈。这种底层能力,是框架和工具替代不了的。