3.3 存储层次优化技术


3.3 存储层次优化技术

本节摘要:层次结构的全部收益可以折算进一个公式——平均访问时间;本节把硬件侧的预取、多级、写缓冲与软件侧的局部性改造放进同一把尺子里量,让每项优化都可计算、可验证。

映射机关上一站拆完了,本站回答"怎么让它转得更好"。这是本章主线的收官站:从数据被请进 Cache,到整座金字塔稳定高效地运转,中间隔着一批可调节的旋钮——有的在硬件设计图上,有的就写在你的循环代码里。

让命中率说话

一切讨论挂在一把尺子上。设命中率为 H、命中访问时间为 Tc、缺失代价为 Tm,平均访问时间为:

Tavg = H × Tc + (1 - H) × Tm

代入真实数量级感受权重的悬殊:Tc 约 2 纳秒,Tm 约 70 纳秒。命中率 95% 时 Tavg 约 5.5 纳秒;掉到 90%,Tavg 立刻涨到 9 纳秒,性能损失超过六成。缺失代价几十倍于命中时间,这决定了优化命中率永远比优化命中速度值钱。 多级结构把公式再叠一层:一级缺失不必直奔主存,先问二级;二级的命中率只需"还行",一级就能维持"很快"的体感。分级的另一层收益是过滤:一级守住速度,末级兜住容量,各级材料各干各的擅长。

硬件侧的三个旋钮

预取:既然空间局部性稳定成立,就在 CPU 索要之前把下一行悄悄搬好。硬件预取器盯着访问流的步长,识别出规律后提前发起总线读。它的赌注是"猜了要用",猜对白赚一拍,猜错白占带宽——所以预取器只认简单规律,乱序访问的增益趋近于零。

写缓冲:写回策略下,脏行回写主存要占用几十纳秒的总线周期。写缓冲充当收发室:CPU 把脏行丢进缓冲就继续跑,缓冲排队慢慢清。代价是 3.2 节提过的悬空期——一致性问题在多设备共享总线的系统里必须正面处理,这也是第 4 章总线仲裁与第 6 章 DMA 的伏笔。

多体交叉:主存被拆成几个独立体,地址轮流分发到各体。连续访问时,上一次的读写周期还没走完,下一次已经在另一个体里开始——像收费站的并行车道,把串行等待摊薄成流水。带宽近似翻倍,延迟却没有变。

软件侧:把数据喂到嘴边

硬件旋钮出厂即定死,程序员真正的杠杆在访问模式。最有效的一手叫分块:处理大矩阵时,不要整行整列地扫,切成能整块装进 Cache 的小块,块内做完所有运算再换下一块。看一个示意:

/* 朴素版本:B 的跨步访问把 Cache 来回冲刷 */ for (i = 0; i < N; i++) for (j = 0; j < N; j++) c[i][j] += a[i][k] * b[k][j]; /* 修正:内层应固定 k 扫 j */ /* 分块版本:以 T 为块长,把三块数据圈进 Cache 反复消化 */ for (ii = 0; ii < N; ii += T) for (jj = 0; jj < N; jj += T) for (kk = 0; kk < N; kk += T) for (i = ii; i < ii + T && i < N; i++) for (j = jj; j < jj + T && j < N; j++) for (k = kk; k < kk + T && k < N; k++) c[i][j] += a[i][k] * b[k][j];

块长 T 的取值依据是三级容量的乘法:三个子块(加一行 a、一列 b、一块 c)合计应小于一级缓存的可用容量。T 取得太大装不下,白忙;太小则块切换开销占比上升。这类参数没有万能值,用 1.3 节的方法实测扫一遍块长,曲线的谷底就是答案

第二手是对齐与填充:把热数据结构对齐到行边界、把可能被同时访问的热字段分家,避免"伪共享"——两个核心各改各的变量,却因为住在同一行里互相把对方的行打成无效。多核时代这是高频性能陷阱,排查手段也简单:把嫌疑字段补上隔离间距再测一遍。

案例展开:一次"优化"的翻车与翻身

背景:同事听信"展开循环必快",把内层循环展开四路,结果测试反而慢了约一成。操作:按本章方法归因——先算理论:循环体本就以整行为访问粒度,展开不减少缺失次数;再看产物:展开后指令体积膨胀,反而把热循环里的另一段代码挤出了指令缓存。结果:撤回展开,改为调整数组布局让关键数组按行对齐,实测提速约两成。解读:优化必须对准瓶颈——这个例子的缺失来自访问布局而非指令数量,展开动错了靶子;指令缓存同样是缓存,代码体积也是"数据"。变式:若瓶颈真是循环开销本身(体积极小、次数极多),展开才值得出手——先测量,再归因,最后动手,顺序不能颠倒。

两级结构的算术与伪共享的数字

把平均访问时间的公式叠到两级。设一级命中率九成、命中两纳秒;一级缺失后二级命中率八成、二级命中十二纳秒;二级也缺失则进主存、七十纳秒。逐层期望:一级命中贡献零点九乘二=一点八纳秒;一级缺失但二级命中贡献零点一乘零点八乘十二=零点九六纳秒;两级全缺贡献零点一乘零点二乘七十=一点四纳秒。合计约四点二纳秒——对照单级直奔主存的七点二纳秒,分级的收益写成了数字。每个参数的改善价值都能按这条公式折成纳秒,这就是"可计算"的含义。

伪共享也值得用数字感受一次。两个核心各自反复更新自己的计数器,两个计数器恰好住在同一行:每次更新都把对方的行打成无效,对方下一次访问被迫跨核取行——热点循环里的每次自增从一拍膨胀成上百拍。修法廉价得惊人:把两个计数器隔开,各占一行,性能即刻恢复。定位手法也简单:剖析发现热点在几行看起来毫无成本的代码上,且与多核并行度正相关,就该怀疑伪共享。

高频追问两则

问:预取会不会把有用数据挤出 Cache? 会,这就是预取的赌注。预取器因此被设计得保守:只认稳定步长,只占用空闲的内存通道,突发性的乱序访问它宁可袖手旁观。工程上若发现预取反而拖慢了负载,多半是访问模式 truly 无规律——先治模式,再谈预取。

问:软件预热(先摸一遍数据)有意义吗? 对首次访问延迟敏感的场合有意义:把该装的行提前装好,正式处理时全是命中。但它是一次性买卖,不改善稳态命中率;稳态不行还是得回头修访问模式。预热是化妆,布局才是骨相。

本节要点回顾

  • 一个公式管全部:缺失代价几十倍于命中时间,命中率是唯一大头。
  • 多级是过滤而非堆料:各级守住各自的 "快"与"大"。
  • 预取认规律:步长稳定的访问流才能被预取器押中。
  • 写缓冲省时间也埋雷:悬空期与 DMA 的一致性冲突要在总线上解决。
  • 软件的第一杠杆是访问模式:分块、对齐、防伪共享,先测量后动手。

第 3 章到此收束。数据的家安顿好了,下一章处理"谁有权在总线上说话"——总线系统与互连。


作者与出处
原作者: 灏天文库
来源:灏天文库
整理: 灏天文库整理
由灏天文库平台收录,内容或由平台用户上传,仅供学习交流
发布者: 作者: 灏天文库 转发
评论区 (0)
U