尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

CPU微架构:超标量与超线程,突破单指令流的两条路径

CPU微架构:超标量与超线程,突破单指令流的两条路径 这篇继续聊CPU微架构。上一节我们把流水线讲透了从取指到写回指令像流水线上的零件一样被重叠加工吞吐率一下子提升了好几倍。但我在评论区看到很多朋友都在问同一个问题流水线已经这么厉害了为什么还要搞超标量、超线程还有人在纠结8核16线程里的“16”到底是不是物理核心超线程和多核到底有多大区别。这一节就把这两个概念放在一起讲清楚——它们是CPU在“单指令流”这个限制下突围的两条关键路径也是看懂CPU天梯图、跑分软件和云主机规格的基础。如果你看腻了厂商PPT里那些“多任务更强”的宣传话术想真正理解CPU是怎么做到的这篇就是给你准备的。1. 为什么“单指令流”会成为瓶颈先理解指令级并行1.1 从标量到流水线的第一次突破早期CPU确实是“老实人”。一条指令从内存取出来经过译码、执行、访存、写回全都搞定了才去取下一条指令。这种方式叫标量执行逻辑简单、实现容易但一颗晶体的利用率其实低得可怜执行单元在算加法的时候内存总线闲着内存总线在取数的时候执行单元又闲着。流水线解决的就是这个“闲着”的问题。它把一条指令拆成IF取指、ID译码、EX执行、MEM访存、WB写回几个阶段让不同指令在不同阶段同时进行。好比一个汽车装配车间不再等一辆车全部装完再装下一辆而是让车在工位之间滚动前进。第一辆在喷漆第二辆在装内饰第三辆刚上底盘。这样虽然单辆车的总装时间没变但车间单位时间产出的车辆数大幅提升。流水线本质上做的是“时间重叠”。它仍然是一个周期取一条指令、一个周期执行一条指令只不过这些指令在时间轴上错位推进。真正让CPU每个周期能同时处理多条指令的是靠超标量。所以在微架构演进里流水线只是第一步它打开了“重叠执行”的思路但没有突破“单指令流”。1.2 流水线也会“空转”依赖、分支和访存延迟流水线理论上可以做到每个周期完成一条指令但现实是它经常饿肚子。程序里的指令不是孤立的它们之间有各种依赖关系。数据依赖是最常见的一种。比如a b c接着d a * 2第二条指令必须等第一条算出a才能开始。一旦有这种依赖后边的指令没法进入执行阶段流水线就出现气泡stall。控制依赖也一样分支指令要等前面比较结果出来才知道往哪跳如果提前取了后面的指令猜错了还要全部丢掉重来。访存就更头疼一级缓存没命中可能要去二级缓存、三级缓存、甚至内存里取一次cache miss的时间足够流水线空转几十上百个周期。所以单指令流不是“太慢”而是“太闲”。就算你设计一条完美无缺的流水线它每个周期也只能喂给执行单元一条指令执行单元大部分时间都在等数据、等分支、等内存。要想真正榨干芯片上的晶体管必须让CPU同时处理多条独立的指令流。这就引出了两条经典路线超标量和超线程。关于“独立”这个词我多说一句。所谓独立就是指令之间没有数据依赖、控制依赖可以并行执行。编译器、CPU硬件、程序员其实都在找这种“独立性”。超标量是在一条线程内部找超线程是在多条线程之间找。方向不同目的完全一致把执行单元喂饱。2. 超标量让一条流水线变成“一组流水线”2.1 核心思路每个周期发射多条指令超标量Superscalar最直白的定义CPU在一个时钟周期内能从指令流中取多条指令并同时分派到多个执行单元上执行。注意区分“发射”和“执行”。发射是指CPU把一条指令送入执行流水线的动作。普通流水线一个周期只发射一条超标量CPU有多个发射端口比如4发射就是最多每周期同时把4条指令送进不同的执行单元。这等于把原来一条流水线扩展成一组并行流水线。实现上前端取指和译码部分要做成多个通路每周期取4条或更多指令并行译码。后端则要有足够的执行端口比如有两个整数运算单元、两个访存单元、一个浮点单元这样4条指令进来才真的能同时开工。如果一个核有8个执行端口但每周期只喂2条那也是浪费。现实里主流消费级CPU的发射宽度普遍在4到6条之间苹果的M系列做到了8条左右服务器高端产品也不会再无限往上加。为什么不能更宽后面我会讲宽度增长带来的复杂度和功耗是非常夸张的。给你一个生活类比一间银行原来只有一个柜台所有人排队办业务。流水线相当于增加了“取号”机制但本质还是一个业务员在同时处理几笔业务的不同阶段。超标量则是直接把柜台从1个变成6个只要客户之间的业务没有关联就能同时办理。前提是——你得保证来的客户手里都有完整的材料。如果大家办的都是同一套需要前一个结果的手续6个柜台也只能看着。2.2 支撑超标量运转的三个关键机制真正做到“多发射”不是简单复制几条流水线就行。程序里的指令顺序不能乱但莫名其妙的关系又会影响并行。这里有三套核心机制是绕不开的。第一寄存器重命名。假如两条指令R1 R2 R3和R1 R4 R5它们都要写同一个寄存器R1。硬件上如果真按顺序执行后一条必须等前一条写回否则结果就乱了。但这两条指令本身毫无数据依赖完全可以并行算。这时候硬件会偷偷把两条指令的“目标寄存器”改名一个写物理寄存器P1一个写P2最后再按程序顺序把结果提交给逻辑寄存器R1。这就是寄存器重命名它消除了WAR写后读和WAW写后写两种“伪相关”。第二乱序执行。有了重命名还得让指令不按程序顺序执行。CPU里有一块“保留站”Reservation Station相当于调度器它每周期扫描所有等待执行的指令挑出“所有操作数都已就绪”的送进执行单元。没就绪的继续等。同时有一个“重排序缓冲”Reorder BufferROB记录指令的原始顺序保证结果提交时严格按程序序来一旦中途发生异常或分支预测错误能安全回滚。这个设计我非常喜欢它像一个餐厅厨房厨师不按点单顺序做菜而是先做那些材料都齐的菜但出菜口还是按原单号摆放。第三分支预测。多发射消耗指令的速度太快了前端必须持续供应指令。遇到分支如果等条件计算出来再取指流水线就断粮了。所以硬件会做分支预测猜一个方向提前取指。猜对了万事大吉猜错了就要把错误路径上已执行、已提交的指令全部作废从正确地址重新开始。这部分吞吐损失在分支密集的代码里非常可观分支预测器的好坏对超标量CPU的最终性能影响极大。这三个机制相互配合缺一不可。寄存器重命名提供“虚假障碍”的绕行乱序执行把“能并行的指令”选出来分支预测保证前端不断粮。它们共同保证了一件最重要的事每周期喂给执行单元的指令尽可能都是真能同时开工的。2.3 现实里的限制收益递减与复杂度爆炸既然超标量这么好为什么不直接做到16发射、32发射答案很简单硬件复杂度不是线性增长而是指数爆炸。发射宽度翻倍意味着每周期要读更多的寄存器操作数、写更多的结果寄存器文件的读写端口成倍增加这些端口的物理布线面积和延迟都在飙升。旁路网络Bypass Network也就是执行单元之间互相传递结果的电路复杂度更是会随执行单元数量平方级增长。你加宽一次主频就得往下降一档功耗按比例上升最后可能花了两倍的晶体管性能只提升10%。另一个更本质的限制是指令级并行ILP本身有限。一个程序里真正没有依赖、能同时执行的指令比例是有上限的。就算硬件再宽软件里找不出那么多独立指令发射槽照样空着。这也是为什么到了某个宽度之后CPU设计者更愿意把钱花在缓存上或者干脆多塞几个核心。所以你现在看任何一颗桌面CPU几乎没有追求极致发射宽度的因为性价比太低。苹果M系列宽度大一点还做到了相对较高的能效那是靠极其复杂的前端和重排序缓冲撑起来的一般厂商学不来的。3. 超线程SMT把执行单元的空闲时间“租”出去3.1 解决什么问题当流水线“缺活干”超标量的思路是“把一个线程里能并行的指令都找出来”。但现实情况是找不全。再聪明的乱序执行也填不满发射槽尤其是碰上cache miss、分支预测错误、锁等待这种“干等”的场景执行单元全部闲置。一个线程的指令流说白了就是一桶水超标量再怎么摆弄一桶水也倒不满一排水槽。超线程的想法很朴素既然一桶水不够那就多接几桶水进来。超线程的通用名叫SMTSimultaneous Multithreading直译是“同时多线程”。Intel管它叫Hyper-Threading超线程本质上完全是一回事。它的核心设计是在一个物理核上同时维护多个线程的上下文让硬件调度器在每个周期从这些线程的指令里挑选能执行的指令填进执行单元。线程A在等内存线程B的指令正好可以上执行单元线程B遇到分支了线程A的指令顶上。我还想强调“同时”这两个字。如果只是让两个线程轮流用一段执行时间那是时间片轮转操作系统本来就能做。SMT的精髓是“同时”填在一个周期内发射槽可能一半属于线程A、一半属于线程B。它不是把1个核分成两个更慢的核而是让同一个物理核的多个执行单元尽量每一刻都有人在用。这个比喻可能不恰当但很贴切一个手艺很高的厨师有8个灶眼如果只有一个顾客点菜他一次最多同时烧两三个锅但如果同时来了三桌客人他就能把8个灶眼全部用上。对顾客来说上菜可能稍微慢一点点但餐厅总的产出翻了好几倍。3.2 硬件上复制什么、共享什么SMT在硬件上要做的第一件事是复制“架构状态”。每个逻辑线程必须拥有自己独立的一套程序计数器、通用寄存器、控制寄存器、中断返回地址等。这是让操作系统感觉“这里有两个CPU核”的前提。操作系统把一个逻辑核调度了任务切换线程时保存/恢复的寄存器组在硬件上就已经是独立的了省了不少切换开销。而执行单元、缓存、TLB、分支预测器这些资源是多个线程共享的。共享的意思是它们不在物理上复制而是动态分配。线程A用得多线程B就少用一点线程B在等缓存线程A就可以独享整个核的所有执行端口。这种“按需分配”让资源利用率大幅提升。那么成本呢增加一个逻辑核的晶体管开销远小于增加一个物理核。Intel早期技术资料里的说法是超线程增加的面积大概是5%左右。面积只有5%能让多线程性能提升15%-30%这买卖当然划算。操作系统在开启SMT的CPU上看到的逻辑核数量是物理核数乘以每核线程数。一颗8核16线程的CPU就是每个物理核提供了2个逻辑核。在Windows任务管理器里你会看到16个CPU图块Linux的lscpu里会显示Thread(s) per core: 2。3.3 能提升多少什么场景划算SMT不是免费的午餐它是拿共享资源换吞吐。到底能赚多少取决于负载类型。经验值是这样的通用多线程负载比如编译代码、压视频、跑数据库事务SMT通常能带来15%到30%的整体性能提升。这类负载指令混合多样既有整数运算又有访存执行单元不会被单一指令类型占满SMT就能钻空子把不同类型的指令插空填进去。但对某些“特种兵”型负载SMT收益很小甚至为负。浮点密集的HPC程序所有线程都在拼命用浮点执行单元共享的浮点单元早就满载了多一条线程只能排队再加上缓存争用、内存带宽争用性能提升可能只有5%。更极端的是高并发下的锁竞争场景两个逻辑线程频繁访问同一个锁变量SMT反而会因为缓存行抖动和额外调度开销拖后腿我之前就碰上过这种案子后面会展开讲。所以在评估一颗CPU该不该开SMT时千万别用“核心数翻倍”来推断性能。你要问的是我的应用是吃执行单元还是吃缓存和内存带宽前者SMT帮不上太多后者往往收益明显。理解了这一点你在买云主机看规格时的很多疑问都会迎刃而解。4. 两者结合后的真实系统设计、观察与实验4.1 真实CPU如何组合超标量和SMT现代CPU里超标量和SMT不是二选一而是叠加使用的。一颗物理核先通过多发射前端每周期取多条指令再通过乱序调度把指令分给多个执行端口如果开了SMT这个调度器还要同时从多个线程的指令池里挑。我来拆一个典型的大核设计。Intel的现代大核比如Golden Cove通常是6发射宽度的乱序超标量核心同时支持2线程SMT。前端每周期从两个线程里按一定策略取指译码后进入统一的调度队列后端的所有执行端口对两个线程动态开放。线程A的指令没就绪调度器就找线程B的指令充数。这就是为什么在单线程负载下开不开SMT没区别因为另一个线程根本没有可调度的指令但多线程一上来填充率立刻不一样。各家的策略又不完全相同。AMD的Zen系列走了类似路线每个物理核也是2线程SMT。IBM的Power系列更激进Power8/9支持SMT8一个物理核可以以8个逻辑线程形式出现特别适合数据库这类并发访问极密集的企业级场景。ARM阵营则相当谨慎多数Cortex-A系列消费级大核直接不做SMT宁可把晶体管花在更多小核上但在服务器方向的Neoverse某些型号里加入了SMT支持。这说明SMT不是天然更优而是架构师在面积、功耗、场景之间的取舍。移动端是最典型的反例。手机SoC追求的是能效比一颗A大核的绝对性能要求没那么高但功耗和发热红线卡得很死。如果开SMT多线程负载下虽然吞吐提升但同样面积和功耗预算下不如直接塞两颗小核来得划算。所以你在手机跑分里几乎看不到SMT带来的加成。4.2 怎么在系统里“看见”超线程理论讲多了我们来点实际的。怎么看一颗CPU有没有开超线程不同系统各有办法。Linux下最直接的是lscpu。看里面Thread(s) per core这一栏如果是1说明没开SMT如果是2或者更多说明开了。再配合Core(s) per socket和Socket(s)就能算出总的逻辑CPU数逻辑CPU数 插槽数 × 每插槽物理核心数 × 每核心线程数。想看每颗逻辑CPU和物理核心的对应关系用cat /proc/cpuinfo里面core id相同的processor就是同一个物理核上的兄弟线程。比如8核16线程的机器core id0到7分别对应两个processor编号。Windows下更简单打开任务管理器→性能→CPU右键图表改“将图形更改为→逻辑处理器”能看到每个逻辑核的占用情况CPU-Z里也会直接显示“Threads: 16”。这里有个很多人踩过的坑你以为的“16核”其实是16个逻辑核物理核只有8个。云主机水更深厂商标“8核16线程”是厚道的不少云服务器直接标“8 vCPU”实际可能是8个超线程逻辑核也就是4颗物理核。买之前最好跑一下lscpu确认很多性能问题其实是从规格误读开始的。4.3 可复现的对比实验开SMT和关SMT差多少与其信厂商宣传不如自己测。这里给你一个我自己常用的实验方法十几分钟就能出结果。第一步准备一个负载。推荐用编译内核或者sysbench这类多线程工具。编译内核最能反映通用混合负载命令大概是make -j$(nproc)记下总耗时。sysbench更省事先装好然后跑CPU基准多线程模式下测每秒事件数。第二步先开着SMT跑一遍记下性能数字。然后进BIOS找到“Simultaneous Multithreading”或者“Hyper-Threading Technology”关掉重启再跑一遍同样的负载。如果你懒得重启Linux内核参数也可以动态试验在启动命令行加nosmt即可临时关闭SMT。第三步对比两组数据。以我实测的经验通用多线程负载打开SMT后性能提升通常落在10%-20%这个区间。假设8核开SMT从8线程变16线程如果性能只提升5%说明负载早就把执行单元、缓存或内存带宽打满了多出来的逻辑线程只是在排队抢资源如果提升有25%说明这个负载的并行度是充足的SMT吃得很饱。这个实验的另一个价值是帮你理解应用特性。同样是数据库纯读场景SMT收益明显一旦写比例上来、锁竞争加剧SMT收益就可能骤降。我见过不少压测报告在“超线程开关对比”这个环节翻车原因就是没分清负载类型。5. 常见误区和排查心得5.1 四个高频误区先帮你排雷第一“超线程就是多核”。这个错得太经典了。多核是物理上增加了执行资源超线程只是在同一个物理核内复用了空闲能力。8核16线程的性能上限无论如何都不可能等于16颗完整物理核顶多接近12到13颗核的水平。跑分网站的天梯图如果直接按线程数排你看看就好别当真。第二“逻辑核翻倍等于性能翻倍”。逻辑核只是提供了更多的调度入口真正的执行单元没有成倍增加。缓存、内存带宽、乱序窗口全是共享的翻倍的不是能力是排队资格。第三“SMT一定能提升性能”。如果负载已经把某个共享资源打满比如浮点单元、内存带宽SMT反而会放大争抢。遇到这类负载关了SMT可能更稳。第四“SMT是独立的一层硬件”。它没有增加执行单元只是让调度器能从多个指令流中选指令。你买CPU看“线程数”本质是看它有没有吃透多线程负载的能力而不是多了几份硬件。5.2 当性能上不去时怎么判断瓶颈在哪遇到多线程程序跑不满、CPU占用率又很高的情况很多人第一反应是加核、加频率。但真正要回答的问题是瓶颈到底在前端、执行单元还是缓存/内存Linux下用perf stat能给出很好的线索。简单跑一个程序后执行perf stat ./your_program重点关注几个指标stalled-cycles-frontend表示前端取指/译码无法供上指令的周期比例如果这个值很高可能是分支预测差或者指令缓存缺失stalled-cycles-backend表示后端执行单元无法开工的周期比例如果这个值高多半是访存延迟或数据依赖在拖后腿。结合SMT来判断就更有意思了。如果一个物理核两个逻辑线程都满载但stalled-cycles-backend仍然很高说明执行单元在等内存加线程也解决不了问题该优化的是数据局部性或者换更大缓存的CPU。反过来如果关了SMT之后stalled-cycles-frontend大幅上升说明单线程的前端供指令能力不够开SMT正好用另一个线程的指令补齐。这套方法比单纯看CPU利用率要精准得多。CPU利用率只能告诉你“有没有在跑”perf能告诉你“在哪里浪费时间”。5.3 用实际经验收尾SMT不是一定要开最后讲一个我自己的真实案例。之前调一个高并发的缓存服务8核16线程的机器压测时发现开了SMT的P99延迟反而不如关闭SMT。一开始我也纳闷理论上多线程并发能力应该增强才对。追查下去发现这个服务的瓶颈不在CPU执行单元而在共享内存结构上的原子操作竞争。两个逻辑线程频繁对同一个缓存行做compare-and-swapSMT打开后两个线程在同一个物理核上竞争同一个端口反而加剧了锁的抖动。把SMT关掉每个物理核只跑一个线程冲突少了P99立刻降了下来。所以我现在每次做性能压测都会把“开SMT/关SMT”作为一个正交变量先跑一轮。这个习惯救了我很多次。SMT是CPU微架构里性价比非常高的设计但它是为“资源有余、线程不足”的场景准备的。如果你的应用本身就把执行单元喂得很满或者瓶颈在锁竞争和缓存一致性上超线程不仅帮不上忙还会添乱。理解超标量和超线程的区别最大的价值不是跑分好看而是你能在系统设计的岔路口做出正确选择什么时候投更多核心什么时候靠提高单核IPC什么时候该开SMT什么时候该关掉它。这些决策背后其实都是对“单指令流限制”这个本质问题的理解。希望这篇能帮你打通这层窗户纸。
返回列表