想象你是一家大排档的老板,雇了一位手艺极好的大厨。可惜厨房设计很离谱:所有食材都放在十米外的仓库,大厨每炒一道菜都要来回跑好几趟拿番茄、鸡蛋、盐和葱姜蒜。菜本身几十秒就能出锅,但跑腿却让客人等上十分钟。 现代计算机的处境一模一样。CPU 这位“大厨”算数极快,但数据和程序却存放在远离它的内存里。每一次“取数—算完—写回”的数据搬运,都在消耗时间和能量。在很多 AI 芯片和超算系统中,数据搬运的能耗占总能耗的 60% 甚至更高——真正的瓶颈不是“算”,而是“搬”。 这篇文章用“厨房改造”的故事线,带你理解数据搬运优化的核心思想:如何让数据被反复复用、如何分块搬运、如何设计存储层次、如何软硬协同、如何调度数据流。学完之后你会发现,现代 AI 加速器里的种种精巧设计,本质上都是在替大厨“少跑腿”。
对 AI 从业者:无论你做模型训练、推理优化还是芯片架构设计,理解数据搬运优化都能帮你从“算力不够”的迷思中走出来,看到系统真实瓶颈所在。
可视化演示
交互展示存储墙能耗瓶颈、三种数据流复用模式与存储层次延迟对比,理解AI芯片数据搬运优化核心方法。
本节配有交互动效(进入视口后自动加载,约 3KB)。
存储墙能耗瓶颈 数据搬运能耗远超计算能耗 搬运与距离和层级正相关 E_move >> E_calc 数据搬运是AI芯片能效首要瓶颈
搬运瓶颈现状
深夜的大排档,大厨阿强要炒一盘番茄炒蛋。他每放一样东西都要跑到十米外的仓库:拿番茄、拿鸡蛋、拿盐、拿糖……配料明明就几样,一趟趟跑下来却让客人等上十分钟。他炒菜只要三十秒,剩下九分半都耗在跑腿上了。
现代通用计算机的“冯·诺依曼架构”同样把“厨房”和“仓库”分开了:CPU 负责计算,内存储存数据,两者之间通过总线搬运数据。CPU 的时钟频率在过去二十年提升了几十倍,但内存访问延迟只下降了几倍——这就形成了著名的“存储墙”问题。墙内的人再能算,墙外的数据进不来,一切都是空转。
我们用一个量化视角看:一次 32 位算术运算的能耗约 1 个单位,而一次访问 DRAM 的能耗可达 12 个单位;内存访问延迟通常是流水线计算延迟的 50 到 200 倍。系统总时间由计算时间和搬运时间组成,而搬运时间往往占据大头。
阿强厨艺再精进,也抵不住仓库的遥远。数据搬运就是这种“看不见的跑腿”,它决定了菜品的真实出货速度。要想让系统变快,光升级“厨师”远远不够,必须从减少跑腿次数开始。
- 计算能耗基准\(E_{compute}\) 表示执行一次加减乘除等算术运算的能耗,这里把它归一化看作 1 个单位,作为全系统的能耗标尺
- 搬运能耗的差距\(E_{move}\) 是数据在内存与 CPU 之间移动一次的能耗,约等于 12 次算术运算的代价
- 总时间构成\(T_{total}\) 由计算时间 \(T_{compute}\) 和搬运时间 \(T_{move}\) 相加得到,两者随执行顺序可能部分重叠
- 搬运占比\(\eta\) 衡量搬运在总耗时中的比例,数值越接近 1,说明系统把绝大部分时间都花在了取送数据上
数据复用原理
老师傅阿强有个习惯:每天开工前先把葱姜蒜切好,分装在小碟子里。炒三盘菜,用的都是同一碟葱花。外人以为他动作快,其实真正秘诀是“一份食材多次用”。
程序访问数据也有类似的规律,叫作“局部性原理”。时间局部性指同一个数据在短时间内被反复访问,就像同一碟葱花连续出现在三盘菜里;空间局部性指一段时间内访问的地址彼此邻近,就像做菜时盐和糖总放在一起、每次都会同时用到。CPU 的缓存正是利用这两点,把常用数据留在身边。还有一个概念叫复用距离:同一数据两次访问之间隔了多少次其他访问,距离越小,数据越可能还留在缓存里。
衡量数据复用效果的关键指标是复用因子 \(R\)。假设程序总共要访问 100 次数据,如果缓存命中后每次都在缓存里读,而真正从内存搬运只有 10 次,那么 \(R = 10\)。编译器通过循环变换、数据重排等技术,尽量让 \(R\) 变大。
同一碟葱花,在三道菜中反复出现,就是时间局部性在厨房里的体现。数据复用做得越好,从仓库取货的次数越少,大厨能专注炒菜的时间就越多。
- 总访存次数\(N_{access}\) 表示程序运行期间对数据发出的所有读写请求总数,这是程序天然需求的体现
- 真正搬运次数\(N_{fetch}\) 表示数据块从慢速存储真正搬到快速存储(如缓存)的次数,也就是缓存未命中次数
- 复用因子\(R\) 是二者的比值,等于一次搬运平均被后续访问利用了多少次,它是优化数据搬运的核心指标
分块策略优化
然而,食材碟子再多,灶台前的台面也有限。阿强的对策是“按菜单分装”:把一周的食材按每道菜的分量装进小袋,每袋恰好塞进台面的小抽屉。每次做菜只抽一袋,用完整理好再换下一袋。
在循环计算中,数据总量通常远大于缓存容量,于是缓存不断驱逐旧数据、加载新数据。分块策略(Blocking/Tiling)把大循环切成小块:让每一块数据在被移出缓存之前完成尽可能多的计算,从而降低搬运总量。实现时通过重排内外层循环顺序,即访存重排,让块内访问尽量集中,提高缓存命中。
假设原始搬运量为 \(N \times M\),其中 \(N\) 为计算步数,\(M\) 为每步需要的数据字节数。分块后,每块数据被复用 \(B\) 次,搬运总量降到原来的 \(1/B\)。但块大小必须满足 \(B \le C\)(\(C\) 为缓存容量),否则块装不进缓存,搬运次数反而增加。实际工程中还会预留若干缓存路给其他数据,块大小常取 \(C\) 的一半甚至更小。
小袋食材刚好塞进抽屉,一次取一袋、一袋做三道菜——这就是分块的精髓:既不多占台面,也不反复跑仓库。
- 朴素搬运量\(N \times M\) 表示不分块时,每次计算所需的全部数据都被当成新数据读取,搬运总量等于计算步数乘以每步数据量
- 分块复用倍数\(B\) 表示一块数据被装入缓存后,能在块内支撑多少次计算,它是分块收益的直接来源
- 容量约束块大小 \(B\) 必须小于等于缓存容量 \(C\),否则块还没用完就被挤出缓存,复用无从谈起
- 优化后的搬运量\(Q_{block}\) 表示分块之后的真实搬运总量,它比朴素方案缩小了 \(B\) 倍
分层存储设计
阿强最终请来设计师翻新厨房:最常用的盐、油、铲子挂在灶台上方;每周要用的干货放中岛柜;只有每月才用的大汤锅放进储物间。离灶台越近的东西,取用越快。
计算机的存储也按“离 CPU 的远近”排列成层次结构:寄存器、L1/L2/L3 缓存、主存、磁盘。越靠近 CPU 的层次越快、越贵、容量越小。程序运行时,数据在各层之间流动,系统感受到的平均访问时间取决于命中率——也就是数据在快速层被找到的概率。
平均访问时间由命中率 \(H\)、快速层延迟和慢速层延迟共同决定。\(H\) 越接近 1,系统越像快速层一样迅速。近数据计算是“把厨师派到仓库门口做菜”,在内存附近放置计算单元,减少原始数据的长途搬运;存算一体更进一步,直接在存储阵列内部完成计算,让数据根本不用离开“仓库”。
厨房布局决定了做菜体验,存储层次决定了程序性能上限。哪怕算法再完美,把食材放在“够不着”的地方,一切都白搭。
- 命中率\(H\) 表示数据访问命中快速存储层(如缓存)的概率,命中率越高,等待越短
- 快速层延迟\(T_{fast}\) 是数据已在缓存中时的访问开销,通常只有几个时钟周期
- 慢速层延迟\(T_{slow}\) 是数据在主存或更远层次时的访问开销,包含总线传输与存储阵列等待
- 平均访问时间\(T_{avg}\) 以命中率为权重,对快慢两种延迟做加权平均,是评估存储层次设计的关键指标
软硬协同优化
光有厨房布局还不够。阿强请了一位帮厨小芳:她提前看菜单,把每道菜需要的配料切好、腌好、按顺序摆在小碗里。阿强只管下锅翻炒,两人配合得天衣无缝。
软件和硬件也可以这样“打配合”。软件侧,编译器和算法工程师负责优化数据布局和循环结构:把一起使用的数据放到相邻地址,通过循环变换让访问顺序更规则,减少缓存失配;硬件侧,缓存预取器根据历史访问模式预测未来的数据请求,提前把数据搬到缓存中。访存调度则决定多个读写请求的排队顺序,让总线不会被单个请求堵死。
当搬运与计算足够重叠时,整体耗时从“相加”变为“取最大值”。这就是为什么预取和流水线能显著提升性能——总工作量没变,但时间被“藏”到了计算背后。
帮厨提前备菜、大厨连续炒菜,总工作量没变,客人等待的时间却大幅缩短。软硬协同,本质上是让一份工作“有人干的时候,另一份工作也在干”。
- 计算时间\(T_{compute}\) 表示所有算术逻辑动作完成所需的时间,它由指令数和处理器吞吐能力共同决定
- 搬运时间\(T_{move}\) 表示所有数据搬运动作完成所需的时间,它由搬运数据量和总线带宽决定
- 重叠后的总时间当预取或流水线让搬运与计算并行时,总时间取两者中的较大者,而不是简单相加
数据流调度
周末流水席,阿强把厨房改成三条工作线:小芳负责切菜,阿强负责炒,小李负责装盘。三个人各干各的,只要每人稳定完成自己的工序,整条流水线就能源源不断出菜。
数据流调度研究的就是:在给定硬件资源下,如何安排读写请求与计算任务的先后顺序,让不同硬件单元尽量并行工作。比如先把下一块要算的数据提前读出(预取式调度),或把不依赖当前结果的任务插入搬运等待的空档,避免“人等菜”或“菜等人”的空闲。
把“取数据—计算—写回”看成流水线的三级。当任务量大时,第一个任务需要一段启动时间才能流出流水线,之后每个任务只需等待最慢一级的耗时 \(\tau\)。任务数越多,启动开销被摊薄得越干净,系统整体吞吐率就越接近 \(1/\tau\)。
流水席的成功不靠某一位师傅的手速,而靠整条流水线的节奏。数据流调度优化的正是这种“节奏感”,让搬运、计算、写回像流水线一样无缝衔接。
- 任务总数\(N\) 表示进入流水线的数据块或任务数量,数量越大,启动开销越容易被摊薄
- 启动开销\(T_{startup}\) 是流水线从空到满所需的时间,它等于流水线级数 \(k\) 乘以级间节拍
- 级间节拍\(\tau\) 是流水线最慢一级的耗时,它决定了整个流水线的最大节奏
- 总耗时第一个任务在 \(k\tau\) 后完成,之后每 \(\tau\) 时间完成一个新任务
- 吞吐率极限当 \(N\) 足够大时,启动开销被分摊,单位时间完成的任务数趋近于 \(\tau\) 的倒数
AI 应用场景
AI 推理引擎中的算子分块
阶段:模型部署与推理优化
解决的问题:Transformer 模型推理时,权重矩阵和激活值频繁进出显存,访存带宽成为吞吐瓶颈。传统的逐元素计算没有充分利用缓存,导致大量重复搬运。通过“分块策略优化”,AI 编译器把大矩阵切成能装进共享内存的小块,让数据在片上被反复复用。
具体案例:在 NVIDIA GPU 上用 Triton 编写矩阵乘法算子,将 4096×4096 的矩阵按 64×64 分块,块数据在 Tensor Core 上复用数百次,使算力利用率从约 20% 提升到 80% 以上。
数据仓库的列式存储与分区设计
阶段:大数据分析与特征工程
解决的问题:数据仓库在做全表扫描时,冷热数据混杂会导致大量无效磁盘 IO。数据布局优化把同一查询常用的字段排在一起,层次化存储设计让热数据驻留内存、冷数据落盘,减少了约 70% 的搬运量。
具体案例:某广告推荐系统的特征表从行存改为列存,并按用户 ID 分区后,单轮训练前的特征查询耗时降低约 4 倍,GPU 集群的空闲等待明显减少。
端侧 AI 芯片的存算一体设计
阶段:AI 芯片架构设计
解决的问题:智能手机上跑人脸识别时,数据传输能耗远超计算能耗。近数据计算和存算一体把卷积乘累加电路嵌入 SRAM 存储阵列内部,让计算直接在“仓库”里完成,避免了大规模数据搬运。
具体案例:某存算一体 AI 芯片在 SRAM 内完成卷积运算,能效比相较传统"CPU+DRAM"方案提升约 10 倍,同时把时延从毫秒级降到微秒级。
Python 演示
class MiniCache: def __init__(self, cap): self.cap = cap self.items = [] self.miss = 0 def access(self, addr): if addr in self.items: self.items.remove(addr) self.items.append(addr) return True self.miss += 1 if len(self.items) >= self.cap: self.items.pop(0) self.items.append(addr) return False def build_window(n, tile): seq = [] if tile == 0: cells = [(r, c) for r in range(n) for c in range(n)] else: cells = [] for rb in range(0, n, tile): for cb in range(0, n, tile): for r in range(rb, min(rb + tile, n)): for c in range(cb, min(cb + tile, n)): cells.append((r, c)) for r, c in cells: for dr in range(-1, 2): for dc in range(-1, 2): rr = r + dr cc = c + dc if 0 <= rr < n and 0 <= cc < n: seq.append(rr * n + cc) return seq def main(): n = 64 cap = 128 tile = 8 naive_cache = MiniCache(cap) tile_cache = MiniCache(cap) naive_seq = build_window(n, 0) tile_seq = build_window(n, tile) for addr in naive_seq: naive_cache.access(addr) for addr in tile_seq: tile_cache.access(addr) naive_miss = naive_cache.miss tile_miss = tile_cache.miss save = (1 - tile_miss / naive_miss) * 100 print("图像尺寸: %dx%d" % (n, n)) print("缓存容量: %d 分块: %dx%d" % (cap, tile, tile)) print("朴素按行扫描搬运次数: %d" % naive_miss) print("分块扫描搬运次数: %d" % tile_miss) print("搬运量下降: %.1f%%" % save) if __name__ == "__main__": main()
MiniCache(cap)模拟一个容量为 cap 的缓存,缓存满时淘汰最久未使用的数据,对应正文的缓存层次。build_window(n, 0)生成按行扫描 3×3 邻域的访问序列,代表不做任何优化的朴素顺序。build_window(n, tile)生成按 tile×tile 分块扫描的访问序列,对应分块策略优化。if addr in self.items命中缓存时把该数据挪到最近使用位置,表示无需从内存搬运。self.miss += 1每次未命中都代表一次真实的数据搬运,这是我们要优化的核心指标。naive_cache.access(addr)让朴素访问序列依次经过缓存,统计它的搬运次数作为基线。save = (1 - tile_miss / naive_miss) * 100算出分块策略省下了多少搬运量,量化优化效果。
常见误区
误区缓存容量不够大所以才频繁搬运,我把缓存从 64KB 换成 64MB,数据搬运问题自然就解决了。
正解缓存容量增大确实能降低未命中率,但成本和功耗也会急剧上升。更关键的是提高数据的复用率:分块、循环重排、数据布局优化可以让小块缓存发挥出接近大缓存的命中率。真正的优化是让同样的数据少被搬运,而不是无限扩大存放空间。
为何常见:大家习惯用“容量”理解缓存,却忽略了访问模式对命中率的决定性影响。容量只是硬件参数,访问顺序才是软件可控的变量。
误区分块越大,一次搬进缓存的数据越多,复用次数越多,搬运总量应该越少,所以块越大越好。
正解块大小必须不大于缓存容量,否则块内数据还没算完就被挤出缓存,复用无从谈起。实际中还要考虑缓存路数、替换策略和多程序干扰,块大小常常取缓存容量的一半甚至更小。块大了反而会因为装不下而频繁换入换出,搬运量不降反增。
为何常见:直觉上“一次多搬点”似乎能减少搬运次数,但忽略了缓存容量这个硬约束。就像小抽屉塞不进大袋子,硬塞只会把手卡住。
误区存算一体是革命性新技术,既然能在存储里直接算,那传统分层存储和缓存机制早晚被淘汰。
正解存算一体在卷积、矩阵乘等数据搬运主导的场景有明显优势,但它会带来面积开销、灵活性下降和通用性受限等问题。常规程序仍需要通用计算单元和分层缓存。最优设计往往是混合架构:该就近算的就近算,该通用算的交给 CPU/GPU。
为何常见:技术热点报道常常放大新架构的覆盖面,让人误以为它能对所有场景通吃。实际工程中很少有银弹方案。
误区CPU 自带硬件预取器,会自动把数据提前搬到缓存里,软件做不做数据布局和循环优化都无所谓。
正解预取器依赖可预测的访问模式,遇到链表跳转、稀疏索引等不规则访问时预取几乎失效。软件侧的数据布局和循环变换是预取成功的前提,只有数据访问足够规律,预取器才能精准预测。两者配合,才能把搬运隐藏到计算身后。
为何常见:底层硬件细节不透明,初学者容易高估硬件的自动优化能力,低估编译器与算法设计对性能的决定性影响。
面试题
Q1. 在冯·诺依曼架构中,造成系统总开销最大的通常是什么?
- A.算术运算单元的执行能耗
- B.内存与计算之间的搬运开销← 正确答案
- C.指令译码与分支预测的能耗
- D.寄存器读写所产生的时钟开销
答案:B。冯·诺依曼架构把计算和存储分开,数据每次在内存和 CPU 之间往返都像厨师去仓库跑腿。搬运能耗和延迟都远高于单次算术运算,因此系统开销最大的环节往往是数据搬运。
Q2. “时间局部性”指的是下面哪种现象?
- A.一段时间内反复访问同一个数据← 正确答案
- B.一段时间内访问相邻的若干个地址
- C.不同时间片访问完全不同的数据
- D.访问时间随数据地址周期变化
答案:A。时间局部性对应“同一个数据短时间内被反复访问”,就像同一碟调料在连续几道菜中反复使用。选项 B 描述的是空间局部性,C 和 D 与局部性原理无关。
Q3. 分块优化中,块大小 B 必须满足什么约束?
- A.B 等于程序总数据量
- B.B 小于等于缓存容量← 正确答案
- C.B 大于缓存容量的一倍
- D.B 与缓存容量没有关系
答案:B。分块的前提是块能装进缓存。如果块比缓存还大,数据搬入后还没来得及用完就被挤出,搬运次数不会减少。就像小抽屉放不下大袋子,强行放反而更乱。
Q4. 平均访问时间 \(T_{avg}=H \times T_{fast}+(1-H) \times T_{slow}\) 中,命中率 H 升高会使什么发生?
- A.平均访问时间逐渐增大,同时慢速层也变慢
- B.平均访问时间逐渐减小,性能提升← 正确答案
- C.快速层延迟随命中率提高而增大
- D.慢速层延迟随命中率提高而下降
答案:B。H 越接近 1,平均访问时间越接近快速层的低延迟。命中率升高一定会降低平均访问时间;慢速层和快速层本身的延迟是硬件属性,不会因 H 改变。这就像食材放在手边的概率越高,平均取菜时间越短。
Q5. 近数据计算的核心思想是?
- A.把内存搬到 CPU 芯片附近
- B.把计算搬到数据所在地点← 正确答案
- C.把数据复制多份并分发
- D.增加数据搬运的带宽上限
答案:B。近数据计算是把运算单元放到存储附近,例如在内存旁加计算部件,减少原始数据的长途往返。它不改变数据总量,而是让数据少跑路,像把厨师派到仓库门口现场做菜。
Q6. 当搬运和计算完全重叠时,流水线总时间接近哪个值?
- A.搬运时间与计算时间之和
- B.计算时间与搬运中的较大值← 正确答案
- C.搬运时间乘以计算时间
- D.计算时间减去搬运时间
答案:B。搬运与计算重叠时两者并行发生,总时长取决于较慢的一方。像帮厨备菜与主厨炒菜同时进行,出菜时间取决于炒菜或备菜中更长的那段。这就是 \(T=\max(T_{compute}, T_{move})\) 的含义。
Q7. 流水线吞吐率公式 \(TP=N/(T_{startup}+(N-1)\tau)\) 中,当任务数 N 趋于无穷时,吞吐率趋近于?
- A.逐渐减小为 0
- B.趋近于流水线的级间节拍
- C.趋近于级间节拍 τ 的倒数← 正确答案
- D.随任务数 N 线性增长
答案:C。N 趋于无穷时,启动开销被摊薄,\((N-1)\tau\) 近似等于 \(N\tau\),所以吞吐率趋近 \(1/\tau\)。\(\tau\) 是流水线最慢一级的耗时,也就是说系统吞吐由最慢工序决定。
Q8. 以下哪些手段能有效降低数据搬运量?
- A.提高缓存命中率并减少回读← 正确答案
- B.增大数据块的复用次数← 正确答案
- C.提高 CPU 的主频大小
- D.减少缓存容量以省电
答案:AB。提高命中率、增大复用倍数都直接减少实际搬运次数,属于从源头减负。提高 CPU 主频只让计算更快,若瓶颈在搬运,主频再高也无济于事;减少缓存容量会加剧搬运,方向正好相反。
Q9. 软硬协同优化包含以下哪些措施?
- A.编译器循环分块与重排← 正确答案
- B.硬件预取器提前取数← 正确答案
- C.访存调度优化请求顺序← 正确答案
- D.仅增加内存条的容量
答案:ABC。软硬协同包括编译器做的循环分块与重排、硬件预取器,以及访存调度,三者分别从数据布局、提前搬运和请求排队顺序上优化。单纯加大内存条容量属于硬件扩容,不是协同优化手段。
Q10. 关于存储层次结构的说法,哪些正确?
- A.越底层容量越大延迟越高← 正确答案
- B.缓存命中率决定平均延迟← 正确答案
- C.寄存器应制作得尽可能大
- D.各层之间由硬件自动管理← 正确答案
答案:ABD。存储层次越靠近底层,容量越大、延迟越高;命中率直接决定平均访问时间;各层数据搬移主要由硬件自动完成。寄存器容量极小且成本高昂,不可能做得很大,故 C 错误。这就像厨房的储物间越大越远,灶台上的调料盒虽小但最快够到。