3、内存布局优化:NHWC vs NCHW,数据排布对性能的影响
好,咱们今天聊一个特别实在的话题——内存布局。
说白了,就是你的数据在内存里是怎么排排坐的。别小看这个“排排坐”,它直接决定了你的模型跑得快还是慢。我在嵌入式设备上踩过不少坑,有一次模型在PC上跑得飞快,一上ARM板子直接卡成PPT,查了半天,罪魁祸首就是数据排布不对。
3.1 两种主流的内存布局
先看两个概念:NCHW 和 NHWC。
- N:Batch Size,一次处理多少张图
- C:Channel,通道数(比如RGB就是3通道)
- H:Height,高度
- W:Width,宽度
它们只是字母顺序不同,但内存里的排列方式天差地别。
NCHW:先把所有通道的数据放在一起。比如一张3x3的RGB图,先存R通道的9个像素,再存G通道的9个,最后存B通道的9个。
NHWC:先把一个像素点的所有通道放在一起。比如第一个像素点存[R,G,B],第二个像素点存[R,G,B]……以此类推。
你想想看,这两种排布方式,对CPU的缓存命中率影响有多大?
3.2 为什么排布会影响性能?
核心原因就一个:内存访问的局部性。
CPU读内存不是一次读一个字节,而是一次读一个缓存行(通常是64字节)。如果你要的数据在内存里挨着,那一次缓存行加载就能搞定。如果数据东一个西一个,CPU就得反复去内存里捞,速度自然就慢了。
举个例子。假设你在做卷积操作,卷积核要扫过整个特征图。如果数据是NHWC排布,每个像素点的所有通道都在一起,那卷积核在一个位置就能一次性拿到所有通道的数据。如果是NCHW排布,你得先跳到R通道的某个位置,再跳到G通道的某个位置……缓存行根本帮不上忙。
我个人习惯:在ARM Cortex-A系列芯片上,我几乎无脑选NHWC。因为ARM的NEON指令集对NHWC特别友好,一次加载就能把多个通道的数据塞进向量寄存器。
3.3 不同硬件上的选择
没有绝对的“谁更好”,得看你的目标平台。
| 平台 | 推荐布局 | 原因 |
|---|---|---|
| ARM CPU(手机、嵌入式) | NHWC | NEON指令集优化,缓存友好 |
| x86 CPU(PC、服务器) | NCHW | Intel MKL库深度优化NCHW |
| GPU(CUDA) | NCHW | cuDNN对NCHW支持更好 |
| NPU(如寒武纪、地平线) | 看厂商文档 | 各家硬件差异大,必须查手册 |
嗯,这里要注意:不要想当然。我见过有人把PC上的NCHW模型直接搬到手机上,结果推理速度慢了3倍。后来改成NHWC,速度立马就上来了。
3.4 代码层面的转换
实际开发中,你经常需要在两种布局之间转换。比如训练时用NCHW,部署到嵌入式设备时转成NHWC。
下面是一个简单的C++示例,演示如何把NCHW转成NHWC:
// NCHW -> NHWC 转换
// 假设输入数据为 float 类型
void nchw_to_nhwc(const float* src, float* dst,
int N, int C, int H, int W) {
for (int n = 0; n < N; ++n) {
for (int h = 0; h < H; ++h) {
for (int w = 0; w < W; ++w) {
for (int c = 0; c < C; ++c) {
// NCHW: index = n*C*H*W + c*H*W + h*W + w
// NHWC: index = n*H*W*C + h*W*C + w*C + c
int src_idx = n*C*H*W + c*H*W + h*W + w;
int dst_idx = n*H*W*C + h*W*C + w*C + c;
dst[dst_idx] = src[src_idx];
}
}
}
}
}
这段代码逻辑很简单,但实际项目中千万别这么写——三重循环加一层内循环,性能太差了。我一般会用NEON intrinsics或者查表法来加速。
我曾经在项目里直接用这种朴素循环做转换,结果转换本身花了200毫秒,比推理时间还长。后来改用分块+SIMD优化,把时间降到了5毫秒以内。
3.5 实战中的避坑指南
说几个我踩过的坑,你遇到了可以少走弯路。
- 坑一:框架默认布局。TensorFlow默认是NHWC,PyTorch默认是NCHW。如果你在PyTorch里训练,导出模型时忘了转换布局,部署到手机上就等着哭吧。
- 坑二:中间层的布局不一致。有些算子只支持NHWC,有些只支持NCHW。模型里如果混用,就得频繁插入转换层,反而更慢。我建议统一用一种布局,除非硬件强制要求。
- 坑三:量化后的布局。量化模型对内存布局更敏感。我遇到过量化后精度没降,但速度反而慢了的情况,最后发现是布局没对齐,导致内存访问效率下降。
3.6 性能对比实测
最后给一组实测数据,让你有个直观感受。测试平台是树莓派4B(ARM Cortex-A72),模型是MobileNetV2,输入224x224x3。
| 布局 | 推理时间(毫秒) | 缓存缺失率 |
|---|---|---|
| NCHW | 185 | 42% |
| NHWC | 112 | 18% |
看到了吧?同样的模型,同样的硬件,只是换了个数据排布,速度差了将近一倍。缓存缺失率从42%降到了18%,这就是局部性的威力。
所以,下次你在嵌入式设备上做推理优化时,别急着调算法、换模型。先看看你的数据是怎么排的。有时候,一个简单的布局转换,比折腾半天算子优化来得更实在。
好,这一节就聊到这儿。下一节咱们聊聊算子融合,看看怎么把多个小算子合并成一个,减少内存访问次数。