Android Camera Hal中通过Neon指令优化数据拷贝

[复制链接]
发表于 10 小时前 | 显示全部楼层 |阅读模式

马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。

您需要 登录 才可以下载或查看,没有账号?立即注册

×
相机录像时预览流和录像流各跑一遍 sensor 输出,这部分 CPU 开销挺扎眼。ARM Neon 指令集能把图像拷贝的活儿并行起来,省下不少时间。这篇记录用 Neon 完成一帧 NV12 数据拷贝的完整实现,也顺带回答“两个流能不能只拷一份数据”这个问题。



一、背景:录像时的双流开销

Camera apk 在普通相机模式做录像操作时,一般是同时请求两个流,也就是预览流和录像流。当两个流输出的图像格式和分辨率完全相同的时候,能不能用一个流拷贝出另一个流的数据,从而省掉 sensor 输出被处理两次的开销?

我们都知道平时用的 memcpy() 是 CPU 一件件搬的,图像数据一大就非常耗 CPU,那有没有办法把 copy 操作优化一下,让 CPU 占用小一点?

二、用 Neon 实现 NV12 拷贝

这里介绍用 ARM Neon 指令集实现输出拷贝的优化。下面以具体实现来看,Neon 指令到底怎么完成一帧 NV12 图像数据的拷贝。

Android 源码里本身就支持 Neon,只要在用到的模块里引用并调用就可以了,实现如下:
  1. #include <arm_neon.h>   //neon指令相关数据结构和接口定义
  2. void NeonMemcpy(void *dest, const void *src, size_t size)
  3. {
  4.      uint8_t* dst8 = static_cast<uint8_t*>(dest);
  5.      const uint8_t* src8 = static_cast<const uint8_t*>(src);
  6.      //小数据直接拷贝
  7.      if (size < 64) {
  8.          for (; size > 0; size--) {
  9.              *dst8++ = *src8++;
  10.          }
  11.          return;
  12.      }
  13.      // 64字节对齐预处理
  14.      const uintptr_t mask = 0x3F;
  15.      const uintptr_t misalign = reinterpret_cast<uintptr_t>(src8) & mask;
  16.      if (misalign > 0) {
  17.          const size_t align_bytes = 64 - misalign;
  18.          for (size_t i = 0; i < align_bytes; i++) {
  19.              *dst8++ = *src8++;
  20.          }
  21.          size -= align_bytes;
  22.      }
  23.      // 主拷贝循环(每次64字节)
  24.      const size_t chunks = size >> 6; // 64字节块数
  25.      for (size_t i = 0; i < chunks; i++) {
  26.          // 使用基本NEON指令替代vld1q_u8_x4
  27.          uint8x16_t data0 = vld1q_u8(src8);        //从src8加载16个8位数据到data0
  28.          uint8x16_t data1 = vld1q_u8(src8 + 16);
  29.          uint8x16_t data2 = vld1q_u8(src8 + 32);
  30.          uint8x16_t data3 = vld1q_u8(src8 + 48);
  31.          vst1q_u8(dst8, data0);                   //将data0中的16个8位数据存储到dst8中
  32.          vst1q_u8(dst8 + 16, data1);
  33.          vst1q_u8(dst8 + 32, data2);
  34.          vst1q_u8(dst8 + 48, data3);
  35.          src8 += 64;
  36.         dst8 += 64;
  37.     }
  38.     // 处理剩余数据(16字节块)
  39.     size &= 63;
  40.     const size_t vec16_count = size >> 4;
  41.     for (size_t i = 0; i < vec16_count; i++) {
  42.         vst1q_u8(dst8, vld1q_u8(src8));
  43.         src8 += 16;
  44.         dst8 += 16;
  45.         size -= 16;
  46.     }
  47.     // 尾部字节处理
  48.     for (; size > 0; size--) {
  49.         *dst8++ = *src8++;
  50.     }
  51. }
复制代码

三、这段代码具体在搬什么


  • 小于 64 字节的数据直接走字节循环拷贝,不进 SIMD 流程
  • 先做 64 字节对齐预处理:让源地址对齐到 64 字节边界,不够的部分先用单字节补齐
  • 主拷贝循环一次搬 64 字节,vld1q_u8 连续取四段 16 字节,再 vst1q_u8 分四次写回
  • 剩下的按 16 字节块处理,最后不足 16 字节的尾巴再逐字节收尾


四、落地时留意两件事


  • 头文件引用的是 arm_neon.h,Neon 相关的数据结构和接口定义都在这里面
  • 主循环用的是基本 NEON 指令来替代 vld1q_u8_x4,通用性更好


这套 NeonMemcpy 可以直接用在预览流和录像流的那一层,少搬一遍 sensor 数据,同样思路换个图像格式也能照搬。

编辑提示:要接入的话先看 Android 源码里的 Neon 支持范围,按模块引用&调用,改完记得和 memcpy 实测一下耗时再定。
回复

使用道具 举报

登录后关闭弹窗

登录参与点评抽奖  加入IT实名职场社区
去登录
快速回复 返回顶部 返回列表