第二讲:存储层次结构——从寄存器到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不是万能的。它有两个致命弱点:
- Cache Miss(缓存未命中): 如果数据访问模式是“跳跃式”的,Cache就猜不准了。每次都要去DDR拿,性能直接崩盘。
- 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都为你打工。