尧图网络 高端网站定制 · 原创设计
免费咨询热线
400-888-6620
免费获取方案
计算机处理器笔记指南:流水线、缓存与性能实测
1. 先搞清楚计算机处理器笔记到底该记什么计算机处理器笔记这个文件夹名我在自己的目录里见过太多次。第一次建它的时候里面塞满了从各处抄来的名词流水线、超标量、乱序执行、缓存一致性、分支预测、超线程……每个词都能背下来但一旦有人问为什么这段循环把步长改成 64速度就掉了三成脑子里还是空的。这就是典型的名词堆积型笔记它有目录、有定义、有截图唯独没有可以拿出来用的东西。我后来把这套笔记推倒重写了一遍改了三条规矩。第一每条概念必须配一个可以跑出来的现象跑不出来就不写进笔记第二所有参数必须带单位、带数量级、带来源禁止出现很大很快这种词第三笔记的读者预设是半年后的自己那时候细节全忘了只剩结论所以结论必须自解释。这三条听着朴素但执行下来同一个文件夹的内容从考试复习资料变成了能直接查的手册。这篇东西就是把我重写后的笔记体系摊开讲一份处理器笔记应该分几层、哪些概念必须先吃透、哪些参数必须自己算一遍、怎么用几行代码把纸面参数变成可验证的数字、以及那些我记错过、理解偏过、后来踩坑踩明白的地方。读者对象很宽正在学计算机组成原理的学生、需要做性能优化的后端和嵌入式开发者、想弄明白为什么我这个程序换个机器就慢了的人都能从里面挑出能直接用的部分。基础弱一点没关系我会把类比和计算过程都写出来已经有经验的可以重点看第 3 章和第 4 章的参数实测那部分是我个人觉得最值钱的内容。2. 笔记的三层结构概念层、数据层、实测层2.1 为什么单纯抄概念一定会忘人的记忆对定义最不友好对差异和冲突最友好。你背十遍缓存是介于 CPU 和内存之间的高速小容量存储器不如记一句L1 的访问延迟大约是 4 个周期内存是 200 多个周期差 50 倍所以一次 L1 未命中等于白干几十条指令。后者有对比、有数字、有后果大脑会自动把它挂到重要那一栏。所以我把笔记拆成三层。概念层负责回答这东西是什么、为什么存在写的时候强制用一句话完成超过三句就说明我自己还没想明白。数据层负责回答它有多快、多大、多贵全部用表格带单位。实测层负责回答在我这台机器上它到底表现如何附代码、附命令、附原始输出。三层之间用链接互相指看概念的时候能一键跳到实测看实测结果的时候能回查原理。这个结构最大的好处是防自欺。概念层写错了数据层对不上数据层抄来的数字和实测层偏差太大你会立刻发现自己的理解有洞。我遇到过最典型的一次笔记里写着L3 缓存通常几 MB 到几十 MB实测发现我的循环在数组只有 4MB 的时候就开始明显变慢一查才发现那台机器的 L3 是按核分片的单核能用到的有效容量远小于标称值。这条如果只抄参数一辈子发现不了。2.2 一张表定下笔记的目录骨架我现在的目录长这样直接抄作业就行层级目录名记录内容更新频率概念层01-数据通路取指到写回的每一级、控制信号、冒险类型一次性写完偶尔补概念层02-并行机制流水线、超标量、乱序、多核、SIMD 的边界遇到新名词时补数据层03-参数台账各级存储延迟/带宽/容量、周期数、CPI 基准每换一台机器更新数据层04-公式与推导性能公式、Amdahl、缓存地址划分的手算过程补一次就够实测层05-基准代码可复现的 C/汇编小实验每个文件一个结论持续加实测层06-工具命令性能计数器的命令行、输出字段解释持续加杂项07-踩坑记录理解错的地方、修正过程、错误结论最高频07-踩坑记录是我个人认为最有价值的一栏它记录的不是知识是我曾经怎么想错的。人的错误是有模式的同一个思维惯性会反复犯把错误本身记下来比记正确答案的收益高得多。2.3 工具选型为什么我不推荐一上来就用重型工具新手最容易犯的错是上来就装一堆分析工具图形界面点来点去看一堆百分比然后什么都没记住。我的建议是分三段走第一段只用秒表和打印写个循环测耗时这是最原始也最不会骗人的手段第二段加系统自带的性能计数器看周期数、指令数、缓存未命中数把慢拆成为什么慢第三段才上缓存模拟器和指令级分析工具用来验证假设。理由很实在前两段的成本几乎为零任何一台开发机都能跑第三段的学习成本高而且它给的是模型结果不是真实结果模型和真实硬件的偏差本身就会误导人。我在第二阶段泡了大概两个月把常见循环形态的周期数和未命中率摸了个大概再去看模拟器输出的时候才分得清哪些是模型误差、哪些是真问题。还有一条经验笔记里存命令不存截图。截图过半年就过时了命令行复制粘贴永远能用。我给每条命令都配了一行注释说明它测什么、输出里该看哪个字段、正常范围大概是多少这三样凑齐了才算一条合格的笔记。3. 一条指令的完整旅程笔记里必须写透的部分3.1 五级流水不是知识点是分析框架经典的五级划分——取指、译码、执行、访存、写回——几乎所有教材都讲但很多人只把它当成一个要背的名词。实际上它是一个分析框架任何一次性能异常你都可以问这次异常发生在哪一级。取指级出问题通常是分支预测失败或者指令缓存未命中译码级出问题多半是指令太长、译码器宽度不够执行级出问题是功能单元争抢或者数据依赖访存级出问题是缓存未命中或者存储转发失败写回级出问题一般是寄存器端口冲突。我在笔记里给每一级都写了三个字段这一级的吞吐上限是多少条指令每周期、这一级的典型延迟是多少周期、这一级出问题的典型症状是什么。比如取指级的吞吐受限于指令缓存带宽和分支预测器典型症状是代码没改但性能波动大因为分支预测器状态被之前的代码污染了。这种症状 → 定位的对应关系比任何原理描述都更实用因为它直接指导你下一步该干什么。这里有个容易忽略的点现代处理器的流水线远不止五级深度普遍在十几级到二十级之间而且前端和后端是分开的。前端负责取指和译码后端负责调度、执行、写回。前端追求的是源源不断喂指令后端追求的是有活干的时候不闲着。理解这个分工你才能明白为什么分支预测失败代价这么大——它伤的是前端前端一停后端再宽也吃不饱。3.2 三种冒险的解决手段以及它们的代价流水线冒险分三类结构冒险、数据冒险、控制冒险。教材上会给每种冒险一堆解决方案但很少讲代价而工程上所有的选择都是代价的比较。结构冒险是硬件资源不够用比如两条指令同时要用除法单元。解决办法是加资源或者排队。加资源的代价是面积和功耗排队的代价是延迟。我在笔记里记了一句大白话结构冒险的本质是钱不够能花钱解决的都不是难题所以现代处理器在这方面投入很大执行单元一堆一堆地堆。数据冒险是后一条指令要用前一条的结果。最朴素的办法是停顿等结果写回寄存器再继续。但停顿太亏了所以有了旁路转发结果一算出来就直接送到需要它的地方不等写回。转发解决不了的是加载-使用依赖也就是前一条指令是从内存读数据后一条立刻要用因为数据从缓存回来至少要几个周期这时候只能停顿。这一条我在笔记里标了红写循环的时候如果内层是读数组元素然后立刻用它做计算那大概率会吃这个停顿把读取提前几轮预取能缓解。控制冒险是分支导致的。解决办法从静态预测一路发展到动态预测、两级自适应、 tournament 预测器本质都是用历史信息猜未来。猜对了没有代价猜错了要清空流水线代价大约等于流水线深度乘以每周期取指宽度实操中量级在十几个到二十个周期。我在笔记里写了一句结论一段代码的性能很多时候不取决于它算了多少而取决于它的分支有多难猜。3.3 分支预测失败的代价手算一遍才有感觉拿一个具体的例子算。假设一个循环处理 N 个元素循环体内部有一个条件分支这个分支大约一半概率走不同方向也就是预测器基本猜不准。假设流水线深度 15 级每周期取 4 条指令那么一次预测失败的惩罚大约是 15 个周期准确说是前端清空后重新填满的时间实际比这个数略小因为有些级可以复用。那么整个循环多出来的周期就是 N/2 × 15。如果循环体本身只用了 8 个周期那么光是分支预测失败就让它慢了接近一倍。这个计算很粗糙但量级是对的它解释了为什么把难以预测的条件分支改成无分支的算术运算比如用掩码选择代替 if-else在某些场景下能提速。反过来说如果分支是高度可预测的——比如循环计数器的比较猜错的概率接近零——那它几乎不花钱。这就是为什么减少分支这个建议不能无脑套用分支多不等于慢分支不可预测才是慢。我在笔记里放了一条判断标准如果同一个分支在最近十几次执行里方向基本一致它就不是瓶颈别去动它如果它在两个方向之间高频摇摆才值得考虑重构。还有一个常被忽略的因素编译器在生成代码时会做条件移动优化把简单的 if-else 赋值改成无分支的 select 指令这会直接消除分支。你可以通过查看生成的汇编确认这件事有没有发生如果没发生原因通常是两个分支的计算都有副作用编译器不敢动。3.4 乱序执行与寄存器重命名为什么指令顺序和实际顺序不一样乱序执行是很多人笔记里的一句话名词实际上它是现代处理器性能的大头。核心思想是程序规定的顺序是假顺序只要数据的真实依赖满足指令可以随便换位置执行。为了让假顺序和真依赖分开硬件做了一件很聪明的事——寄存器重命名。程序里只有十几个通用寄存器但物理寄存器有上百个。当一条指令写某个逻辑寄存器时硬件给它分配一个新的物理寄存器同时把逻辑名和物理名的映射表更新。这样一来后面读这个逻辑寄存器的指令读到的是新的物理寄存器而更早、还没执行完的指令用的是旧的物理寄存器两者互不干扰。写后写、读后写这两种名字冲突被彻底消除了只剩下真正的数据依赖写后读还需要等。这件事对写代码的直接启发是不要为了省一个变量而复用寄存器。一段循环里反复用同一个临时变量看起来整洁但在编译器眼里可能制造了一串假依赖限制了重排空间。我实测过一个很典型的情况把一个数组求和的累加器从一个改成四个最后再加起来在某些数据规模下速度快了将近一倍原因就是单个累加器形成了一条串行的依赖链而处理器再宽也只能一次算一步四个累加器把这条链拆成了四条功能单元就能并行跑起来。这个技巧在笔记里我单独存了一个代码片段因为它太好用了。乱序执行的另一个副作用是测量失真。你测一段代码的耗时测到的是乱序窗口内所有指令混合执行的结果单独看某一条指令的延迟没有意义。所以笔记里我强调测延迟要构造依赖链测吞吐要构造独立指令流两者不能混着测。4. 存储层次笔记里最容易记错、也最值钱的部分4.1 缓存地址怎么切分手算一次胜过看十遍一个地址送进缓存怎么知道该去哪里找分三段偏移位、索引位、标记位。偏移位决定缓存行内的位置索引位决定去哪个组找标记位用来确认找对了没有。这三个字段的位数不是随便定的由缓存的三个参数决定容量、行大小、组相联度。举个例子手算一遍。假设 L1 数据缓存 32KB、8 路组相联、行大小 64 字节。那么总共有 32KB / 64B 512 行。8 路一组所以组数是 512 / 8 64 组。组索引需要 log2(64) 6 位。行内偏移需要 log2(64) 6 位。剩下的高位全是标记位。如果地址是 48 位那标记位就是 48 - 6 - 6 36 位。这个计算我逼着自己每个机器都算一遍因为算完之后很多现象就自动解释了。比如为什么步长为 4096 字节的访问特别慢4096 2^12偏移 6 位加索引 6 位一共 12 位也就是说地址每次加 4096索引位完全不变全部落在同一个组里。8 路组相联意味着这个组只有 8 个位置第 9 次访问必然把前面的挤出去于是每次访问都是未命中。这就是所谓的冲突未命中它既不是容量不够缓存还有很多空位也不是强制未命中数据刚用过纯属地址映射撞车。理解这一点之后写代码时就有了一个下意识的检查如果多维数组的某一维长度恰好是 2 的幂按列遍历会非常惨。常见的规避手段是给数组每行末尾补几个填充元素把长度变成 2 的幂加一跨度就错开了。这个技巧在数值计算代码里非常常见笔记里我把它记成一句话长度凑 2 的幂是给硬件看的不是给人看的凡是跨行访问都要防一手。4.2 用一小段代码把缓存行为测出来概念再清楚不如自己跑一遍。这是我笔记里最常用的一个实验只需要几十行 C#define _GNU_SOURCE #include stdio.h #include stdlib.h #include time.h static double now_sec(void) { struct timespec ts; clock_gettime(CLOCK_MONOTONIC, ts); return ts.tv_sec ts.tv_nsec / 1e9; } int main(int argc, char **argv) { long stride atol(argv[1]); /* 步长单位元素int 占 4 字节 */ size_t n 64UL * 1024 * 1024; /* 64M 个 int约 256MB */ int *a (int *)malloc(n * sizeof(int)); if (!a) return 1; for (size_t i 0; i n; i) a[i] 1; /* 先写一遍避免懒分配干扰 */ for (int rep 0; rep 3; rep) { size_t idx 0, cnt 0; double t0 now_sec(); while (idx n) { a[idx] 1; idx (size_t)stride; cnt; } double dt now_sec() - t0; printf(stride%-6ld 访问次数%-9zu 耗时%.4fs 单次均摊%.2fns\n, stride, cnt, dt, dt / cnt * 1e9); } free(a); return 0; }编译时记得关掉优化对这个循环的干扰用-O1就够别用-O3否则编译器可能把整个循环优化没了。跑的时候依次给参数 1、2、4、8、16、32、64、128、256、512、1024、2048、4096。我这边实测的规律大概是这样步长 1 到 4 的时候单次均摊最快因为一个 64 字节的缓存行能装 16 个 int顺序访问几乎每次都命中步长涨到 16正好 64 字节的时候每次访问都是新的缓存行但硬件预取器还能识别这种规律速度下降但不夸张步长继续涨到几百预取器彻底失效每次都要真去内存取单次均摊会跳一个量级步长到 4096 的时候除了预取失效还会额外叠加组冲突和页表项压力是最惨的一档。这个实验最大的价值不是那几个数字而是你会亲眼看到慢是怎么一层层叠上去的。笔记里我把三档的均摊时间都抄了下来旁边写上这一档是预取失效这一档是组冲突下次遇到类似的循环脑子里立刻能对上号。4.3 缓存一致性四个状态一句话记住多核环境下每个核有自己的私有缓存同一份数据可能存在于多个核的缓存里怎么保证大家看到的是同一份靠一致性协议。最常见的四状态协议名字叫 MESI四个状态是已修改、独占、共享、无效。死记四个状态很容易混我给自己的记法是按这个核有没有写权限、别的核有没有副本两个维度画一个 2×2 的格子独占 我有别人没有、共享 我有别人也有、已修改 我有别人没有而且我改过、无效 我这份作废了。写操作要求先拿到独占或已修改状态如果当前是共享状态就必须先发消息让其他核把副本置无效等它们确认之后才能写。这个等确认的过程就是一致性流量它走的是核间互联延迟远高于本地缓存命中。这一条对写代码的启发很直接多个线程频繁写同一个缓存行里的不同变量会引发伪共享。两个线程明明操作的是不同的变量逻辑上没有竞争但因为这两个变量恰好落在同一个 64 字节的缓存行里硬件的粒度是整个行于是一个线程写另一个线程的整行就失效来回打乒乓。解决办法是把热变量按缓存行对齐并填充到 64 字节。我在笔记里存了一段填充的写法同时标注了只在真的测出问题之后才用别预防性地到处填浪费内存还影响布局。这是很典型的经验型判断伪共享确实存在但绝大多数代码不是被它拖慢的。5. 性能指标把纸面参数变成能验证的数字5.1 主频、CPI、IPC三者的关系必须自己推一遍评价处理器性能最核心的公式只有一个程序执行时间 指令数 × 平均每指令周期数CPI × 时钟周期时间时钟周期时间的倒数就是主频。IPC 是 CPI 的倒数表示每周期能完成多少条指令。这两个指标一个是一次能干多少活一个是干一件活要几次。主频高但 CPI 高等于来得快去得也快主频低但 IPC 高等于慢工出细活。两条路都能通向高性能只是取舍不同。这条公式我在笔记里被追问过很多次为什么我的程序在 A 机器上跑得快换到 B 机器就慢了B 的主频明明更高答案几乎总是 CPI 变差了。指令数基本由编译器决定主频是硬件给的唯一能波动几十个百分点的就是 CPI。而 CPI 的波动又基本来自两处缓存未命中率和分支预测失败率。所以定位路径是固定的先测 CPI如果 CPI 明显高于预期再去拆缓存未命中和分支失败。这里有个细节值得记下来CPI 是个平均值讨论单条指令的 CPI 没有意义。一条除法指令可能占用十几个周期一条加法指令可能一个周期搞定你测出来的是整个指令流的平均。所以自己的基准测试一定要说明白这个 CPI 是在多长的循环、什么数据规模下测的脱离上下文的 CPI 数字不可比。5.2 用性能计数器把慢拆开系统自带的性能计数器组是个宝库命令行大概长这样perf stat -e cycles,instructions,cache-references,cache-misses,branch-misses,context-switches \ ./cachetest 16输出里要重点看四组数周期数和指令数之比给出 CPI缓存引用数和缓存未命中数之比给出未命中率分支未命中数除以总分支数给出预测失败率上下文切换数可以帮你排除是不是被调度打断了这种低级干扰。我在笔记里写了一张判读对照表比记一堆原理好用现象可能原因下一步CPI 很高但缓存未命中率正常数据依赖链太长、功能单元争抢查循环里的累加器数量、是否有除法缓存引用数本身就低循环太小开销被测量噪声淹没放大数据规模再测未命中率 10% 以上工作集超出某一级缓存对比不同数据规模找拐点分支未命中率超过 5%有难预测的条件分支查数据分布考虑无分支写法上下文切换频繁被其他任务打断绑核重测或降低并发干扰这张表我用了很久绝大部分为什么慢的问题不用往下挖就能定个方向。它的价值在于先排除再深入如果上下文切换很多你分析半天缓存策略都是白费劲先把这个干扰因素去掉。还有一点必须提醒性能计数器在虚拟化环境里可能不准某些事件会被复用或者采样读出来的数字和物理机有偏差。做严谨对比的时候要在同一台机器、同一次开机、同样的负载下测跨机器跨时段的数字只能看量级不能看小数点。5.3 Amdahl 定律为什么多核扩展经常不线性很多人做多核优化的第一反应是四核应该快四倍然后测出来只有 2.3 倍开始怀疑人生。这个结果完全正常原因在 Amdahl 定律里写得很清楚加速比的上限由不可并行部分决定。公式是加速比 1 / (串行比例 并行比例 / 核心数)。假设一个任务有 20% 的部分只能串行执行那么无论你上多少核加速比的上限就是 1 / 0.2 5 倍。八核也不过 3.3 倍左右十六核 4.4 倍越往上收益越小。我在笔记里给这个公式配了一张手算表把串行比例从 1% 到 50% 分别算出 8 核和 32 核的理论加速比。算完那张表之后我对用更多核解决问题这个思路就彻底冷静了**串行比例 10% 的时候32 核只有 7.8 倍而你要为此付出 32 份协调开销、缓存一致性的压力、以及调度器的复杂度。**优化串行部分 5% 的收益往往比多加一倍核还大。这里有个实践层面的补充并行化之后每个核的缓存压力会上升因为多个核同时跑不同的数据块工作集总量翻倍本来刚好塞进 L3 的数据现在塞不下了于是单核性能下降整体加速比进一步打折。这个效应在设计分块策略的时候必须考虑分块大小不能只看平均分还要看每一块能不能装进私有的那一级缓存。6. 指令集与编译优化软硬交界处的笔记怎么写6.1 精简指令集与复杂指令集的边界其实没那么多可争的教材上讲 RISC 和 CISC 的区别能列一页纸但实际工作的视角要简单得多现代处理器的前端在把复杂指令翻译成内部微操作也就是说外面看到的那套指令集是给软件用的里面跑的是一套简化后的东西。从这个角度看指令集的繁简更多影响的是编码密度和译码复杂度而不是执行效率本身。我在笔记里给这一节写了三条实用结论。第一编码密度影响取指带宽指令越长同样字节数能取的指令越少前端压力越大这在移动端和嵌入式上尤其明显。第二译码宽度是硬限制一个周期能译出多少条指令直接决定了峰值 IPC 的上限宽译码器面积大功耗高所以不同定位的芯片在这上面取舍不同。第三向量扩展宽度决定数据并行能力这个对做数值计算的人影响最大后面单独说。关于哪种架构更好这种问题我在笔记里写了一句架构不是性能实现才是。同一个指令集架构可以有完全不同的实现频率、乱序窗口大小、缓存层次、功能单元数量都可以天差地别。抱着某类架构一定更快的想法去做选型基本都会翻车。6.2 向量化笔记里必须存一个能直接抄的例子数据级并行的收益是最直观的也是最容易验证的。原理很简单一个向量寄存器能装 4 个 32 位整数或者说 8 个 16 位整数一条向量加法指令一次算完 4 个加法理论吞吐提升 4 倍。实际做不到 4 倍因为还有加载、存储、数据重排的开销但 2 到 3 倍很常见。写法上最简单的一条路径是让编译器自动向量化把循环写成简单的形态、避免循环体内的函数调用和条件分支、让编译器能证明指针不重叠在 C 语言里加上 restrict 限定符。我笔记里存的模板大致是这样void vec_add(const float * restrict a, const float * restrict b, float * restrict out, int n) { for (int i 0; i n; i) { out[i] a[i] b[i]; } }restrict的作用是告诉编译器这三个指针不会指向同一块内存编译器才敢放心地重排和向量化。如果编译器不能确定这一点它会生成一条保守的路径性能差一截。这个限定符不改变程序语义但改变了编译器的可见信息量属于零成本优化。想确认有没有真的向量化看编译器的优化报告把优化级别开到-O3加上向量化提示选项编译器会逐条打印哪个循环被向量化、为什么没被向量化。原因通常就那么几个循环次数不确定、有潜在别名、有难以向量化的条件跳转。把这几类问题解决掉收益立刻兑现。我在笔记里踩过的一个坑值得说向量化之后数据规模不是向量宽度的整数倍时剩下的尾部处理可能拖慢整体。比如一次处理 8 个元素总共 1000 个最后 8 个要走标量路径同时编译器还要生成一套判断逻辑。当数据规模很小时这套开销可能比向量化省的还多。所以向量化不是无脑开要按数据规模分档小数据走标量路径反而更快。6.3 看汇编不是玄学是核对笔记的手段很多人觉得看汇编太硬核其实只需要看三件事循环体里有多少条指令、有没有意料之外的内存访问、有没有多余的比较跳转。这三件事看懂了80% 的性能疑惑能自己解答。我习惯的做法是拿一个小函数编译成汇编把源码和汇编并排放着对照。第一次做会很痛苦因为寄存器重命名之后变量名全没了所有东西都叫 r0、r1、r2。但看多了会发现规律函数的开头会有一批加载参数的操作中间是计算结尾是一批写回的操作循环体的开头是一个比较加跳转结尾是一个无条件跳回。你只要能把循环体的边界框出来数清楚里面有多少条指令再乘以循环次数就能估算出指令数跟性能计数器读出来的指令数对一对如果差得远说明你漏了什么。这个对照的过程本身就是在校验理解。我笔记里记录过一次某段代码的循环体我数出来 12 条指令但计数器显示平均每个元素 40 条指令。查了半天才发现内层调用的函数没有被内联每次调用都有一次压栈出栈的开销还有一次间接跳转。加上内联之后指令数降到 16 条性能提升了三成。指令数的异常往往是隐藏函数调用的报警器这个信号非常可靠。7. 常见问题与排查技巧实录7.1 我在两类问题上反复栽跟头第一类是测量本身有问题。早年我特别相信秒表测出来的数字直接写进笔记后来发现同一段代码不同次跑能差 30%。原因五花八门CPU 频率在波动热管理调频、后台有别的任务在抢核、内存分配落在不同的页上、甚至是第一次跑的时候缓存是冷的后面是热的。现在我固定了三条规矩预热至少跑一遍再开始计时、取三次中位数而不是平均值、测试时尽量绑核并且关掉无关的后台任务。中位数比平均值靠谱得多平均值会被一次异常长的样本拉偏。第二类是把某一次的现象当成普适规律。最典型的一次是我测出把数组按行遍历比按列遍历快 5 倍写进笔记当成金科玉律结果换个矩阵大小按列遍历反而略快因为那个尺寸下按行遍历恰好撞上了缓存组冲突而按列遍历的跨度反而错开了。教训是性能结论必须带前提条件数据规模、编译选项、机器参数缺一不可。现在我写任何性能结论都会强制带上这三样否则宁可不写。7.2 问题速查表症状高频原因快速验证方式同一段代码两次跑差别很大频率波动、冷缓存、后台干扰预热后取中位数、绑核、连跑十次看方差数据规模变大后突然掉档工作集跨过了缓存层级按 2 的幂次增大规模找掉档的拐点加了并行反而更慢串行比例高、伪共享、任务切分粒度太细单线程耗时统计、按缓存行对齐后再测循环体内什么都没改但速度变了分支预测器状态被污染、地址对齐变化换个调用顺序、检查循环起始地址小规模数据测不出差异测量噪声大于真实差异把规模放大到毫秒级以上再测向量化开了没效果数据规模太小、尾部开销占比高、潜在别名看优化报告、加 restrict、按规模分档换机器性能差异巨大缓存容量、内存带宽、主频策略不同记录两台的参数台账、对比 CPI7.3 几条从坑里爬出来的心得心得一先怀疑测量再怀疑代码。这段话说起来像废话但执行起来能省一半时间。任何异常数据第一反应应该是会不会是测错了检查预热、检查计时方式、检查是否被优化掉这三步花不了五分钟却能避免你花五个小时去分析一个不存在的问题。心得二笔记里要写反例。每条结论旁边尽量记一个什么时候这条不成立。比如减少分支能提速的反例是分支高度可预测时几乎无收益缓存对齐总是好的的反例是填充过多会浪费缓存容量反而降低命中率。有了反例你才不会在不该用的地方硬套。心得三参数台账跟着机器走不跟着结论走。每换一台机器先把缓存层级、容量、行大小、核数抄进台账再重跑一遍基准代码更新那一栏的实测值。纸面参数和实测量级对不上是很常见的事尤其是共享缓存和标称容量之间经常差一截。台账更新完之前所有的性能结论都要打个问号该重测的重测。心得四别为了记笔记而记笔记。我有一段时间沉迷于把每个概念都画成图、写成结构化模板结果花了大量时间在整理上真正啃透的东西没几个。后来改了只记录那些我反复查、反复错、反复用的东西其他一律不记需要的时候现场搜。笔记的价值密度比完整度重要得多。8. 一点个人体会这套笔记体系跑了两三年最大的变化不是我记住了多少名词而是遇到性能问题时的反应方式变了。以前是感觉哪里不对加个日志看看现在是先算一下这个循环的工作集有多大估一下它落在哪一级缓存再估一下分支的规律性然后在脑子里排个优先级。这个反应方式不是看书看来的是一条一条实测堆出来的。如果让我给刚开始建这份笔记的人一句建议那就是先写满一页纸的实测记录再去补那一百页的原理。原理迟早都会懂但实测的直觉必须在你还对硬件保持好奇的时候建立起来。等到你能凭代码结构大致猜出 CPI 落在哪个区间这份笔记就算真正长在你身上了。最后分享一个小习惯。我每加一条新结论都会在末尾留一行空位写着待验证。等哪天在新机器、新数据规模上再撞见同类现象就把那一行补上。有些空了半年才补上有些到现在还空着——而空着的那几条恰好是我最不敢跟别人打包票的部分。这个留白机制比任何整理方法都更能管住我乱下结论的毛病。
RELATED

相关推荐

电力系统潮流计算与开断分析:牛顿-拉夫逊法实践

电力系统潮流计算与开断分析:牛顿-拉夫逊法实践

1. 电力系统潮流计算基础与开断潮流概述电力系统潮流计算是分析电网运行状态的核心工具,它通过求解节点功率平衡方程来确定系统中各节点的电压幅值、相角以及支路功率分布。牛顿-拉夫逊法因其二次收敛特性成为最常用的潮流计算方法,而开断潮流分析则是在…

📅 2026/11/6 2:48:13
MySQL外键约束报错分析与最佳实践

MySQL外键约束报错分析与最佳实践

1. 外键约束报错问题深度解析今天在重构一个学生成绩管理系统时,遇到了一个让我抓狂的外键约束报错。错误提示明确告诉我"Missing unique key for constraint score_ibfk_9 in the referenced table student",但奇怪的是我明明已经移除了所有相…

📅 2026/11/6 2:43:13
UNet图像分割项目落地避坑指南:数据预处理、模型调试与部署全流程

UNet图像分割项目落地避坑指南:数据预处理、模型调试与部署全流程

简介:本资源是一份面向Python图像处理初学者与深度学习入门者的U-Net图像分割实践项目,聚焦生物医学、遥感及卫星影像等场景下的像素级分割任务。资源提供完整可运行代码框架与配套示例数据,涵盖数据预处理、模型构建(含跳跃连接与…

📅 2026/11/6 2:43:13
MORE NEWS

更多资讯

📰

【Spring-响应式编程】什么是反应式编程(Reactive Programming)?它的核心思想是什么?

什么是反应式编程?从“拉”到“推”的范式革命与高并发系统的终极解法 用户问题原文:1. 什么是反应式编程(Reactive Programming)?它的核心思想是什么? 本文将彻底解析反应式编程的本质,不仅回答“是什么”,更揭示其在 金融行情推送、IoT 设备监控、大促订单流处理 等高…

📰

8. 测试-性能测试-工具简介及实战

文章目录 前言 一、工具简介 业务流程压测:JMeter 企业复杂系统:LoadRunner HTTP 极限性能:wrk 简单快速验证:ab 二、JMeter 第一部分:基础教程 🛠️ 第一步:安装适配 Java 8 的 JMeter 1. 选择正确的版本 2. 下载与解压 3. 启动验证 4. 界面汉化(可选) 🚀 第二步:…

📰

【芳芯科技】图像识别教室无人关灯系统

实物效果图:实现功能:1.采用ARM内核STM32单片机作为控制核心。2.采用OV7670摄像头实现图像获取。3.采用TFT2.8显示配合图像调试显示。4.通过按键进行布放,布放后采用图像算法进行检测,检测到有人体后通过电开关进行教室灯光的开启…

📰

OpenCV实战第二篇:用代码给照片做一场精细的“像素级整容”

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

📰

ArkUI -- 状态管理 V1和 V2 混用

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

📰

iloader iOS 侧载教程:免命令行,3 步在 iPhone 装好 SideStore

iloader iOS 侧载教程:免命令行,3 步在 iPhone 装好 SideStore 【免费下载链接】iloader User friendly sideloader 项目地址: https://gitcode.com/GitHub_Trending/iloa/iloader 免费 Apple ID 的证书配额用满、配对文件不知道丢进哪个目录、手…

TODAY

今日更新

THIS WEEK

本周精选

THIS MONTH

本月热门

读完文章,想聊聊您的网站?

告诉我们您的行业与需求,资深顾问一对一梳理方案与报价,全程免费。

📞 💬