第二章:ARM平台基础:Cortex-A系列与M系列架构对比、NEON指令集简介、内存层级与缓存策略

2.1 两个世界:Cortex-A 与 Cortex-M 的基因差异

做嵌入式视觉,你首先得搞清楚你手里的芯片到底是个什么脾气。我刚开始入行那会儿,拿着一个 Cortex-M4 的板子硬跑 YOLO,结果可想而知——帧率感人,发热倒是很积极。后来才明白,A 系列和 M 系列根本就是两个物种。

说白了,Cortex-M 是为「实时控制」而生的。它追求的是确定性的中断响应,功耗极低,适合传感器数据采集、电机控制这类任务。而 Cortex-A 是为「应用处理」设计的,它要跑 Linux、要处理复杂的计算任务,比如视觉算法中的卷积、矩阵运算。

我个人的习惯是:如果项目只需要做简单的特征提取(比如边缘检测、光流),M 系列加个 DSP 核勉强能应付。但一旦涉及到深度学习推理、多目标跟踪,直接上 A 系列,别犹豫。

对比维度 Cortex-A 系列 Cortex-M 系列
典型应用 智能手机、AI摄像头、边缘服务器 传感器节点、电机控制、简单视觉
操作系统 Linux、Android、QNX RTOS、裸机
内存管理 MMU(虚拟内存) MPU(内存保护)或无
缓存层级 L1/L2/L3 多级缓存 通常无缓存或仅有 TCM
指令集 ARMv7-A / ARMv8-A (AArch64) ARMv7-M / ARMv8-M
NEON 支持 标配(从 Cortex-A8 开始) 不支持

嗯,这里要注意:M 系列没有 NEON,这是硬伤。如果你要在 M 系列上做视觉加速,只能靠 Cortex-M7 的 SIMD(单指令多数据)或者外挂 DSP。但效果嘛……我只能说,别抱太大期望。

2.2 NEON 指令集:视觉算法的加速引擎

NEON 是什么?说白了就是 ARM 的 SIMD 扩展。它允许你一条指令同时处理多个数据。比如你要对一张 640x480 的灰度图做像素加法,用普通指令你得循环 307200 次。用 NEON,一次可以处理 16 个 8 位像素,循环次数直接降到 19200 次。

我曾经在一个项目里,用 NEON 优化了一个 3x3 的 Sobel 算子。优化前每帧处理耗时 12ms,优化后直接降到 2.3ms。你想想看,这 5 倍多的提升,完全不需要改算法逻辑,只是把 C 代码换成了 NEON 内联汇编。

核心要点:NEON 寄存器是 128 位宽的。你可以把它看成 16 个 8 位整数、8 个 16 位整数、4 个 32 位整数,或者 4 个 32 位浮点数。视觉算法中,最常用的是 8 位和 16 位整数模式。

来看一个最简单的 NEON 代码示例,实现两个 8 位图像的像素相加:

// 使用 NEON 内联函数实现图像加法
#include <arm_neon.h>

void add_images_neon(uint8_t* src1, uint8_t* src2, uint8_t* dst, int size) {
    int i;
    // 每次处理 16 个像素
    for (i = 0; i < size; i += 16) {
        // 加载 16 个 8 位像素到 NEON 寄存器
        uint8x16_t a = vld1q_u8(src1 + i);
        uint8x16_t b = vld1q_u8(src2 + i);
        
        // 执行加法(饱和加法,防止溢出)
        uint8x16_t c = vqaddq_u8(a, b);
        
        // 存储结果
        vst1q_u8(dst + i, c);
    }
}

这段代码里,vld1q_u8 加载 16 个像素,vqaddq_u8 做饱和加法,vst1q_u8 存回内存。三行指令,搞定 16 个像素的处理。如果用纯 C 写,你得写一个 for 循环,还要手动判断溢出。

我的经验:NEON 编程最容易踩的坑是内存对齐。ARM 架构下,如果数据没有 16 字节对齐,vld1q 系列指令会触发异常。我建议你在分配图像缓冲区时,使用 posix_memalignaligned_alloc 确保 16 字节对齐。

2.3 内存层级:别让你的数据在「长途跋涉」中浪费性能

ARM 平台的内存层级,说白了就是一个「速度与容量」的博弈。寄存器最快,但只有几十个。L1 缓存次之,几十 KB。L2 缓存几百 KB 到几 MB。主存最慢,但容量最大。

我曾经犯过一个低级错误:在 Cortex-A72 上跑一个 5x5 的高斯滤波,每次从主存读取像素,处理完再写回主存。结果性能还不如在 Cortex-M7 上用 TCM(紧耦合内存)跑得快。为什么?因为 A72 的 L1 缓存只有 32KB,我的图像数据太大,缓存一直在「颠簸」——加载、驱逐、再加载。

避坑指南:我曾经在一个双目视觉项目中,把左右两帧图像分别放在两个不同的内存区域。结果每次处理左图时,缓存被左图占满;切换到右图时,缓存全部失效。后来我把两帧数据交错存放,让它们共享缓存行,性能提升了 40%。

ARM 平台的典型缓存层级如下:

  • L1 缓存:分为指令缓存(I-cache)和数据缓存(D-cache)。通常各 32KB。访问延迟 2-4 个时钟周期。
  • L2 缓存:统一缓存,大小 256KB - 2MB。访问延迟 10-20 个时钟周期。
  • L3 缓存:部分高性能芯片(如 Cortex-A76 集群)才有。大小 2-4MB。访问延迟 30-50 个时钟周期。
  • 主存:DDR4/LPDDR4。访问延迟 100+ 个时钟周期。

你想想看,如果算法需要频繁访问一个 1MB 的数据集,而 L2 缓存只有 512KB,那有一半的数据访问都要走主存。这性能损失,可不是一星半点。

2.4 缓存策略:让数据「住」在离 CPU 最近的地方

优化缓存,说白了就是两件事:时间局部性空间局部性。时间局部性是指你刚用过的数据,很可能马上还要再用。空间局部性是指你刚用过的数据附近的数据,很可能接下来要用。

在视觉算法中,最典型的缓存优化手段是「分块处理」(tiling)。比如一个 1920x1080 的图像,你一次处理整张图,缓存根本装不下。但如果你把图像分成 64x64 的小块,每个小块都能塞进 L1 缓存,处理完一块再处理下一块。

// 分块处理示例:3x3 卷积
#define TILE_SIZE 64

void conv3x3_tiled(uint8_t* src, uint8_t* dst, int width, int height) {
    for (int ty = 0; ty < height; ty += TILE_SIZE) {
        for (int tx = 0; tx < width; tx += TILE_SIZE) {
            int end_y = min(ty + TILE_SIZE, height);
            int end_x = min(tx + TILE_SIZE, width);
            
            // 这个小块的数据会留在缓存中
            for (int y = ty; y < end_y; y++) {
                for (int x = tx; x < end_x; x++) {
                    // 执行 3x3 卷积
                    process_pixel(src, dst, x, y, width);
                }
            }
        }
    }
}

嗯,这里要注意:分块大小不是越大越好,也不是越小越好。我一般会先查芯片的 L1 数据缓存大小,然后让每个块的数据量(包括输入和输出)不超过 L1 缓存的一半。比如 L1 是 32KB,那每个块的数据量控制在 16KB 以内。

另一个实用技巧:预取(prefetch)。ARM 提供了 pld 指令,可以提前告诉缓存「我马上要用这个地址的数据」。我在做光流跟踪时,会在处理当前像素的同时,预取下一行像素的数据。这样当真正需要时,数据已经在缓存里了。

// 使用预取指令优化
void process_with_prefetch(uint8_t* data, int stride) {
    for (int i = 0; i < 1000; i++) {
        // 预取 64 字节后的数据
        __builtin_prefetch(data + i + 64, 0, 3);
        
        // 处理当前数据
        process(data[i]);
    }
}

最后说一句:缓存优化没有银弹。不同的算法、不同的数据规模、不同的芯片,最优策略都不一样。我个人的习惯是:先用 perf 工具跑一遍,看看缓存缺失率。如果 L1 缺失率超过 10%,就该考虑优化了。如果 L2 缺失率超过 5%,那基本可以断定算法在「等数据」,性能瓶颈就在内存访问上。

ARM平台视觉算法优化知识体系 视觉仿生算法 算法优化:分块处理、预取、循环展开 ARM平台核心能力 NEON SIMD指令集 缓存层级策略 内存对齐与预取 硬件架构选择 Cortex-A系列(高性能) Cortex-M系列(低功耗) 优化路径:算法 → 指令集 → 内存 → 硬件选型

这张图概括了本章的核心逻辑:从视觉算法出发,经过 NEON 指令集加速和缓存策略优化,最终落实到 Cortex-A 或 Cortex-M 的硬件选型。每一步都环环相扣,缺一不可。

专注资料整理