第二讲:存储层次结构——从寄存器到DDR,数据搬运的代价有多大?

大家好,欢迎来到第二讲。

上一讲我们聊了内存带宽为什么是嵌入式AI推理的“命门”。今天,我们深入底层,看看数据到底是怎么在芯片内部“跑”起来的。从寄存器到DDR,这中间的距离,比你想象的要远得多。

一、存储金字塔:为什么不能只用一种存储器?

你想想看,如果有一种存储器,又快又大又便宜,那该多好。可惜,现实世界没有这种完美的东西。

芯片设计里,我们有个经典的“存储金字塔”概念。越往上,速度越快,容量越小,价格越贵。越往下,速度越慢,容量越大,价格越便宜。

层级 典型容量 访问延迟 带宽(典型值) 每比特成本
寄存器(Register) 几十~几百字节 ~0.3 ns > 1 TB/s 最高
L1 Cache 32KB ~ 128KB ~1 ns ~500 GB/s
L2 Cache 256KB ~ 2MB ~5 ns ~100 GB/s
L3 Cache(片上SRAM) 2MB ~ 32MB ~15 ns ~50 GB/s 中低
DDR SDRAM 256MB ~ 8GB ~50 ns ~10 GB/s

看到这个表格,你可能会问:为什么寄存器那么快,DDR那么慢?

说白了,就是物理距离和材料成本的博弈。寄存器就在CPU核心旁边,走线极短。DDR呢?它在芯片外面,要通过PCB走线、封装引脚,光是信号传输就要花掉好几个纳秒。

核心观点: 嵌入式AI推理的瓶颈,往往不是计算单元不够快,而是数据在存储层级间“搬运”的速度跟不上。

二、数据搬运的代价:一个具体的例子

我记得有一次,我在优化一个轻量级目标检测模型。模型不大,只有2MB,但跑在Cortex-M7上就是卡。我一开始以为是卷积算子的实现有问题,折腾了两天。

后来我用性能计数器一测,发现CPU有70%的时间都在等数据!

为什么会这样?我们算一笔账:

// 假设我们要做一次简单的向量加法
// 数据在DDR中,每次加载一个float(4字节)

for (int i = 0; i < N; i++) {
    float a = data_a[i];  // 从DDR加载,约50ns
    float b = data_b[i];  // 从DDR加载,约50ns
    float c = a + b;      // 加法运算,约0.5ns
    result[i] = c;        // 写回DDR,约50ns
}

看到了吗?一次加法只需要0.5纳秒,但为了拿到数据,我们花了100纳秒等待。计算时间只占0.5%,剩下的99.5%都在等内存!

我的经验: 在嵌入式平台上,for循环里如果频繁访问DDR,性能基本就“交代”了。一定要想办法把数据搬到离计算单元更近的地方。

三、Cache:硬件帮你做的“预判”

Cache就是硬件自动管理的一块高速缓存。它会把最近用过的数据,以及它“猜”你接下来会用到的数据,提前从DDR搬过来。

嗯,这里要注意。Cache不是万能的。它有两个致命弱点:

  1. Cache Miss(缓存未命中): 如果数据访问模式是“跳跃式”的,Cache就猜不准了。每次都要去DDR拿,性能直接崩盘。
  2. Cache污染: 有些数据只用一次,比如中间计算结果。但Cache傻乎乎地把它缓存起来,把真正有用的数据给挤出去了。

我曾经在一个项目中,用了一个很大的查找表。每次推理都要查这个表,但查表的位置是随机的。结果Cache命中率只有10%,性能惨不忍睹。

避坑指南: 如果你的算法有“大跨度随机访问”的特点,比如稀疏矩阵乘法、不规则图计算,别指望Cache能救你。你需要手动管理数据布局。

四、手动管理:TCM和SRAM的妙用

既然Cache靠不住,那怎么办?

很多嵌入式芯片,比如STM32H7系列,提供了TCM(Tightly Coupled Memory,紧耦合内存)。TCM和CPU直连,访问延迟和寄存器差不多,而且没有Cache Miss的问题。

// 在STM32H7上,将关键数据放到DTCM区域
// 使用__attribute__((section(".dtcm_data"))) 指定

__attribute__((section(".dtcm_data")))
float weights[1024];  // 这个数组在DTCM中

// 访问速度接近寄存器级别
for (int i = 0; i < 1024; i++) {
    output[i] = weights[i] * input[i];
}

我个人习惯,把最频繁访问的权重、偏置、中间缓冲区,都放到TCM或者片上SRAM里。虽然容量有限,但能换来几倍甚至十几倍的性能提升。

实战建议: 在项目初期,就规划好哪些数据是“热数据”,哪些是“冷数据”。热数据必须放在TCM或SRAM中,冷数据可以放在DDR里。

五、DDR的“隐藏成本”:刷新与行冲突

你以为DDR的50ns延迟就是全部了?太天真了。

DDR内部是DRAM单元,需要不断刷新(Refresh)来保持数据。刷新期间,你不能访问内存。这大概会占用5%~10%的带宽。

还有更坑的:行冲突(Row Conflict)。DDR内部是按“行”组织的。如果你连续访问同一行,很快。但如果访问不同行,就要先关闭当前行,再打开新行。这个操作叫“行激活”,额外增加20~30ns的延迟。

// 行冲突示例
// 假设地址A和地址B在同一Bank的不同行

read(A);  // 打开行A,读取数据,耗时50ns
read(B);  // 关闭行A,打开行B,读取数据,耗时80ns(多了30ns的行激活时间)
read(A);  // 再次关闭行B,打开行A,又是80ns

你看,如果访问模式是“来回跳”,DDR的实际带宽可能只有理论值的一半。

我的优化技巧: 尽量让数据访问是“顺序”的。比如矩阵乘法,按行访问比按列访问快得多。因为按行访问,DDR的行切换次数少。

六、总结:数据搬运的代价到底有多大?

我们来算一笔总账:

  • 寄存器到寄存器: 几乎零代价,0.3ns搞定。
  • 寄存器到L1 Cache: 代价很小,1ns左右。
  • 寄存器到L2 Cache: 代价中等,5ns左右。
  • 寄存器到DDR: 代价巨大,50ns起步,加上行冲突、刷新,实际可能超过100ns。

说白了,一次DDR访问的时间,足够CPU执行200次加法运算。这就是为什么我们说“内存墙”是嵌入式AI推理的头号敌人。

最后一句: 下次你写推理代码时,多想想数据在哪。是躺在DDR里睡大觉,还是已经在寄存器里等着被计算?这个选择,决定了你的模型是跑在10帧还是100帧。

好,这一讲就到这里。下一讲,我们会聊聊“数据布局优化”,看看怎么把数据摆放得更“聪明”,让Cache和DDR都为你打工。