一个由cpu到内存取数据引发的跨世纪惨案
从CPU到内存:一个引发计算机架构革命的“世纪惨案”
一个由CPU到内存取数据引发的“世纪惨案”,成为了计算机架构演进的起点。CPU的执行速度快到离谱,单位以纳秒(ns)计算,而从内存读取数据、传输到CPU缓存,却要花费60100纳秒——这短短几十纳秒,对CPU来说却像“漫长的假期”,足够它不间断运行200300个生命周期。
就这么点功夫,CPU啥也不干,躺得比谁都舒服。作为“老板”(计算机设计者),看着花钱请来的“核心员工”如此闲置,气不打一处来:真是Big胆!我花大价钱打造你,你居然在等待内存数据的间隙摸鱼?绝对不能让员工闲着!这场CPU闲置危机,倒逼老板想出各种“骚操作”,也开启了计算机架构的革新之路。
第一幕:惨案初现(计算机早期,1940s-1970s)
早期计算机中,CPU与内存的速度差距就已显现,只是随着CPU性能的飙升,这场“惨案”愈发突出:CPU执行一条指令仅需0.3纳秒,但去内存读取或搬运一份数据,却要花费80纳秒。
80纳秒是什么概念?足够CPU不间断执行200多条指令。于是荒诞的一幕发生了:CPU在等待内存数据的间隙,什么也做不了,只能“闲置待命”,躺得比谁都舒服。
“老板”(计算机设计者)看着CPU利用率曲线,气得拍桌子:“我花大价钱打造你,你却在等待中浪费大半时间?” 这场“CPU闲置危机”,成为了后续所有架构革新的起点。
第二幕:老板的骚操作——乱序执行与顺序提交(1960s-1980s)
为了治住CPU“摸鱼”的毛病,老板率先想出第一个骚操作:乱序执行。
老板对着CPU训话:“你不是要去内存搬数据吗?等数据的功夫别闲着,先把后面不依赖这份数据的指令都干了!” 这就是乱序执行的核心逻辑——CPU将指令重新排序,让不依赖等待资源的指令优先执行,把200~300个闲置的生命周期全部利用起来,绝不浪费一个时钟周期。
但很快,新的问题出现了:万一提前执行的指令,最终依赖于等待的内存数据,而数据回来后发现与预期不符,提前算好的结果就成了“无用功”。
于是老板又加了一道保障——重排序缓冲区。所有提前执行的指令结果,都会先存放在这个缓冲区里,等依赖的内存数据回来后,再判断是否需要使用提前算好的结果,确认无误后再正式提交。这就是“顺序提交”,既治好了CPU的“摸鱼病”,又保证了指令执行的正确性。
为了解决CPU等待内存的问题,“老板”率先想出了第一个“骚操作”:乱序执行。
老板说:“你不是要去内存搬A数据吗?等A的时候别闲着,先把后面不依赖A的B、C、D指令先干了!” 这就是乱序执行的核心逻辑——CPU将指令重新排序,让不依赖等待资源的指令优先执行,不浪费一个时钟周期。
但很快,新的问题出现了:万一提前执行的B指令,最终依赖于A数据的结果,而A数据回来后发现与预期不符,提前算好的B结果就成了“无用功”。
于是“老板”又加了一道保障——重排序缓冲区。所有提前执行的指令结果,都会先存放在这个缓冲区里,等依赖的A数据回来后,再判断是否需要使用提前算好的结果,确认无误后再正式提交。这就是“顺序提交”,既解决了闲置问题,又保证了指令执行的正确性。
第三幕:老板的又一骚操作——分支预测(1970s中期,随乱序执行逐步完善)
乱序执行解决了“等待内存时的闲置”,但老板又发现了新的“摸鱼点”:if-else分支指令。
CPU执行到分支指令时,不知道该走if路径还是else路径,只能等待条件判断结果,又陷入了“闲置摸鱼”状态。老板索性破罐破摔:“不管了,先猜一个!猜对了,继续跑;猜错了,就放弃之前的操作,重新来!”
这就是老板的第二个骚操作——分支预测技术。从早期简单的“静态预测”(如默认走if路径),到后来的“动态预测”(根据历史执行记录调整预测方向),现代CPU的分支预测准确率已能达到95%以上。但猜错的那5%,会导致CPU清空已执行的无效指令,成为性能的“滑铁卢”——这也是为了让CPU不闲着,付出的微小代价。
乱序执行解决了“等待内存时的闲置”,但“老板”又发现了新的卡点:if-else分支指令。
CPU执行到分支指令时,不知道该走if路径还是else路径,只能等待条件判断结果,又陷入了“闲置”。老板索性破罐破摔:“不管了,先猜一个!猜对了,继续跑;猜错了,就放弃之前的操作,重新来。”
这就是分支预测技术。从早期简单的“静态预测”(如默认走if路径),到后来的“动态预测”(根据历史执行记录调整预测方向),现代CPU的分支预测准确率已能达到95%以上。但猜错的那5%,会导致CPU清空已执行的无效指令,成为性能的“滑铁卢”——这也是为了减少闲置付出的微小代价。
第四幕:甲方发难——多线程、多进程(1960s末-1980s,操作系统兴起后)
老板的骚操作让CPU不怎么摸鱼了,但甲方(操作系统)很快找上门来,一脸不满:“老板,你这CPU虽然不闲了,但我这边还有一堆活等着干,它还是有空闲时间,我花的钱不白花了?”
甲方转头找到“二道贩子”(操作系统内核),拍着桌子说:“我这些活,你给我多开几个通道,我要让CPU多干点活,不能再让它有空闲的机会!”
于是,多进程、多线程技术应运而生。二道贩子通过“上下文切换”,让多个任务轮流占用CPU,一个线程在等内存数据时,另一个线程就顶上干活,彻底堵死CPU的“摸鱼间隙”。但问题也随之而来:上下文切换本身需要开销——每次切换都要保存当前线程的寄存器状态、切换内存地址空间,耗时几百纳秒,切换过于频繁反而会拖慢整体性能。
随着操作系统(甲方)的出现,新的需求来了:“老板,你这CPU闲着的时候,我的其他任务没法推进,太浪费资源了!”
甲方一拍桌子,想出了一个主意:“你这一个‘工作通道’不够,给我多开几个!一个线程在等内存数据,另一个线程就顶上干活,不让CPU空着!”
于是,多进程、多线程技术应运而生。通过“上下文切换”,让多个任务轮流占用CPU,CPU不再因单个任务等待内存而闲置。但问题也随之而来:上下文切换本身需要开销——每次切换都要保存当前线程的寄存器状态、切换内存地址空间,耗时几百纳秒,切换过于频繁反而会拖慢整体性能。
第五幕:原子操作与CAS诞生(1970s末-1980s,多线程并发问题催生)
多线程普及后,新的“惨案”出现了:多个线程同时操作同一个共享数据(比如“账户余额”),会出现数据错乱——这就是数据竞争。
场景直击:两个线程同时读取余额=100,线程A计算100+10=110并写回,线程B同时计算100-10=90并写回,最终余额要么是90,要么是110,与正确结果100相去甚远。
为了解决这个问题,硬件层面率先突破——CPU厂商推出了原子指令,其中最核心的就是CAS(Compare-And-Swap,比较并交换),这也是无锁编程的硬件基础。
技术专家解释:“既然问题出在‘多个人抢同一件东西’,那我们就把‘抢’这个动作本身变成‘原子性’的——要么抢成功,要么抢失败,不会出现中间状态。”
老板感慨:“这不就是‘谁抢到算谁的,抢不到就重试’吗?连‘钥匙’(锁)都不用了。” 但专家也补充:“CAS有个漏洞——ABA问题:数据的值从A变成B,又变回A,CAS会误以为数据没变过。后来我们给数据加了‘版本号’,才算彻底解决了这个问题。”
第六幕:锁的诞生与代价(1980s,CAS普及前的过渡方案)
在CAS等原子操作尚未普及、且多线程并发问题日益突出的年代,“老板”想出了一个简单直接的解决方案——锁。
老板一拍桌子:“你们两个不能同时碰这个共享变量!谁先拿到‘钥匙’(锁),谁进去修改,另一个在外面等着,不许插队!”
锁的本质,是把“多通道”(多线程)临时变成“单通道”,保证同一时刻只有一个线程能访问共享数据,从根本上解决数据竞争。但锁也带来了新的代价,成为了新的性能瓶颈:
当锁竞争激烈时,大部分线程都在等待锁释放,CPU又陷入了“闲置”,效率甚至不如单线程
一群人排队用一个水龙头,大部分人都在排队,水龙头的利用率反而很低
优先级反转
低优先级线程先拿到锁,高优先级线程需要等待低优先级线程释放锁,导致高优先级任务被阻塞
VIP通道被普通人占用,VIP只能排队等待,失去了优先级优势
锁的代价,让人们重新重视起CAS等原子操作,无锁编程也逐渐成为并发编程的重要方向——这是一场“从有锁到无锁”的回归与升级。
第七幕:IO模型的演进(1980s-2000s,解决CPU等待I/O的核心方案)
除了等待内存,CPU另一大“闲置场景”就是等待I/O(如磁盘读写、网络请求)——I/O操作的耗时远比内存访问更长,动辄毫秒级,足够CPU执行数百万条指令。为了解决这个问题,IO模型经历了三次关键演进,逐步实现了“不让CPU等I/O”的目标。
第一次尝试:非阻塞IO(1980s早期)
“老板”让员工(CPU)不断去询问I/O设备:“数据准备好了吗?”“准备好了吗?”——员工全程跑腿询问,看似没闲着,实则什么有效工作都没做,反而被频繁的询问耗尽精力。这就是非阻塞IO,虽然避免了“原地等待”,但CPU的开销反而更大,并未真正解决问题。
第二次尝试:IO多路复用(1980s中期,select诞生;2000s,epoll完善)
为了解决非阻塞IO的CPU浪费问题,人们想出了一个办法:雇一个“工头”(IO多路复用机制),让一个工头同时盯着所有I/O任务,CPU只需要偶尔问问工头“有没有任务完成”,其余时间可以专心做自己的事。
这就是IO多路复用,早期的select、poll,以及后来Linux推出的epoll,都是这个思路。Nginx、Redis等高性能中间件,至今仍在使用IO多路复用模型,它能高效处理大量并发I/O请求,让CPU的利用率大幅提升。
第三次尝试:异步IO(2000s后,逐步成熟;2019年,Linux io_uring推出)
IO多路复用虽然高效,但CPU仍需要“主动询问”工头任务状态,还是有轻微的闲置。于是,真正的异步IO应运而生:员工(CPU)给工头(异步IO机制)交代好I/O任务,就撒手不管,专心做自己的事;等工头把数据处理好、送到CPU面前,再通知CPU来处理。
这就是“真正的异步IO”——CPU完全不需要等待I/O,全程专注于计算任务。Linux在2019年推出的io_uring,就是目前最成熟的异步IO方案,大幅降低了I/O操作的延迟,进一步释放了CPU的性能。
第八幕:甲方的对策——协程登场(1980s提出,2010s后普及)
多线程虽然进一步压榨了CPU的空闲时间,但甲方很快发现,二道贩子(操作系统内核)的收费实在太昂贵了——每次线程上下文切换,都要进入内核态,保存大量寄存器状态,耗时几百纳秒;线程开得越多,切换开销越大,反而拖慢性能,相当于“为了让CPU不闲,花了更多冤枉钱”。
甲方一拍大腿,想出了更省钱的办法:“既然二道贩子收费太贵,那我们自己来调度!固定几个通道(内核线程),然后让我们自己人(协程)在这些通道里轮班上岗,不用麻烦二道贩子,省下来的开销全是我们的!”
这就是协程——用户态的轻量级线程,切换完全在用户态完成,不需要进入内核,耗时仅几十纳秒,远低于线程切换。2010s后,协程逐步普及:Go语言的goroutine、Rust的async task、Python的asyncio,都是协程的具体实现,既让CPU彻底不闲,又不用花二道贩子的“冤枉钱”,成为解决高并发、低延迟场景的核心方案。
多线程虽然解决了CPU闲置问题,但“二道贩子”(操作系统内核)的收费太高——每次线程上下文切换,都要进入内核态,保存大量寄存器状态,耗时几百纳秒;线程开得越多,切换开销越大,反而拖慢性能。
甲方(操作系统/开发者)一拍大腿:“既然内核切换太贵,那我们自己来调度!固定几个‘通道’(内核线程),然后让我们自己的‘员工’(协程)在这些通道里轮班上岗,不用麻烦内核!”
这就是协程——用户态的轻量级线程,切换完全在用户态完成,不需要进入内核,耗时仅几十纳秒,远低于线程切换。2010s后,协程逐步普及:Go语言的goroutine、Rust的async task、Python的asyncio,都是协程的具体实现,成为解决高并发、低延迟场景的核心方案。
第九幕:异步语法的终极优化——async/await(2010s中期,随协程普及诞生)
协程和异步IO普及后,新的问题出现了:早期的异步编程依赖回调函数,代码嵌套层级多、可读性差(被称为“回调地狱”);后来的Promise(承诺)虽然缓解了回调地狱,但仍不够直观。
甲方(开发者)说:“Promise还是不够简单,我想要像写同步代码一样写异步代码,不用记复杂的回调逻辑!”
于是,async/await语法糖诞生了。编译器会把“看起来同步”的async代码,自动编译成一个状态机——每个.await点,就是状态机的一个切换节点,当遇到I/O等待时,自动切换到其他协程,等待完成后再切换回来继续执行。
这就是Rust异步的“零成本抽象”——你写的高层异步代码,会被编译器优化成高效的底层状态机,既保留了同步代码的可读性,又拥有异步编程的高性能。
第十幕:Go和Rust的不同选择(2010s后,协程与异步编程的两大主流方向)
随着协程和异步编程的成熟,两大主流语言Go和Rust,选择了两种不同的技术路线,适配不同的应用场景,本质上都是为了更高效地利用CPU、减少闲置:
维度
Go
Rust
协程模型
有栈协程(goroutine)
无栈协程(async task)
栈大小
2KB起步,可动态增长
编译时确定,仅几十字节,更轻量
调度方式
Go运行时自动调度,开发者无需关心
需要第三方执行器(如tokio),开发者可灵活控制
抢占机制
有(10ms强制切换,避免单个goroutine长期占用CPU)
无(仅在.await点主动让出CPU,更高效但需开发者注意)
通俗比喻:Go的思路是“你随便写,我来调度”,降低开发者门槛;Rust的思路是“你告诉我哪里可以切换,我听你的”,追求极致的性能和灵活性。
终幕:世纪惨案的终极答案(跨越数十年的演进总结)
回到最初的“世纪惨案”:CPU等待内存(或I/O),闲置200-300个时钟周期,甚至更长时间。从1960s到2020s,跨越数十年的计算机架构演进,所有技术的终极目标只有一个:把“等待”的时间藏起来,或者用“等待”的时间干别的活,不让CPU浪费一分一秒。
我们可以用一张表,清晰梳理所有核心问题与解决方案,以及它们的出现顺序:
核心问题
解决方案(按出现时间排序)
核心思想
出现年代
CPU等待(内存/I/O),陷入闲置
乱序执行+顺序提交
等待时,优先执行不依赖等待资源的指令
1960s-1980s
分支预测
猜测分支路径,避免因等待分支判断而闲置
1970s中期
多进程/多线程
一个任务等待时,切换另一个任务占用CPU
1960s末-1980s
IO多路复用(select/epoll)
一个“工头”盯着所有I/O,CPU无需主动等待
1980s中期-2000s
协程
用户态轻量级切换,减少线程切换开销
1980s提出,2010s普及
异步IO(io_uring)+ async/await
CPU完全不等待I/O,异步任务自动调度,代码直观高效
2000s后-2010s
多线程并发,数据竞争
锁
临时将多通道改为单通道,保证数据访问唯一性
1980s
原子操作(CAS)+ 无锁编程
让“抢数据”的动作原子化,无需锁也能保证正确性
1970s末-1980s
一句话总结:整个计算机体系结构的演进,就是一部“不想让CPU闲着”的血泪史。从硬件层面的乱序执行、CAS,到软件层面的多线程、协程、async/await,每一次技术突破,都是为了填补CPU与内存、I/O之间的速度鸿沟,让每一个时钟周期都发挥最大价值。
作者: 咕咚!
出处: https://www.cnblogs.com/linga/
关于作者:专注虚拟化,运维开发,RPA,Rust,Go,Python!
本文版权归作者和博客园共有,禁止*.csdn.net转载,禁止以盈利为目的的转载,转载文章,但未经作者同意必须保留此段声明,且在文章页面明显位置给出, 原文链接 如有问题, 可邮件(oldsixa@163.com)咨询.

浙公网安备 33010602011771号