3、内存布局优化:NHWC vs NCHW,数据排布对性能的影响

好,咱们今天聊一个特别实在的话题——内存布局。

说白了,就是你的数据在内存里是怎么排排坐的。别小看这个“排排坐”,它直接决定了你的模型跑得快还是慢。我在嵌入式设备上踩过不少坑,有一次模型在PC上跑得飞快,一上ARM板子直接卡成PPT,查了半天,罪魁祸首就是数据排布不对。

3.1 两种主流的内存布局

先看两个概念:NCHWNHWC

  • 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%,这就是局部性的威力。

所以,下次你在嵌入式设备上做推理优化时,别急着调算法、换模型。先看看你的数据是怎么排的。有时候,一个简单的布局转换,比折腾半天算子优化来得更实在。

好,这一节就聊到这儿。下一节咱们聊聊算子融合,看看怎么把多个小算子合并成一个,减少内存访问次数。