如何高效将uint8_t*加载到AArch64 NEON的uint16x8(x3)寄存器?
我需要将uint8_t*类型的8位源数组元素加载到AArch64 NEON/ASIMD寄存器中,目标格式为uint16x8_t或更优的uint16x8x3_t——每个字节要以16位短整型形式存入寄存器,且循环每次迭代都要加载新批次的数据。
目前我用的方法是先通过vld3_u8加载成uint8x8x3_t,再用vmovl_u8宽化转换得到uint16x8_t,但觉得这种方式效率偏低;另一种手动把字节转成uint16_t再加载的方法性能更差。
想请教有没有更高效的实现方式,或者我是不是漏了合适的ASIMD intrinsic?比如能否直接把每个8位元素加载为寄存器中的16位值,让寄存器里存的是{5,33,102,153...}这类16位值?我的循环代码如下:
void foo(uint8_t* bgr, uint16_t width, uint16_t height) { for (uint16_t y = 0; y < height; y++) { for (uint16_t x = 0; x < width; x += 8) { // 此处需将8位值加载为16位值,有无更高效方法? uint8x8x3_t bgrChunk = vld3_u8(bgr); uint16x8_t b = vmovl_u8(bgrChunk.val[0]); uint16x8_t g = vmovl_u8(bgrChunk.val[1]); uint16x8_t r = vmovl_u8(bgrChunk.val[2]); bgr += 24; // ... 基于加载数据的操作 } } }
高效实现方案
1. 你的现有思路其实已经接近最优
vld3_u8(对齐内存下)是单周期加载指令,vmovl_u8是零扩展字节到半字的单周期指令,CPU的乱序执行会自动隐藏这两个操作的延迟,实际开销并没有你想象的大。不要怀疑这个基础路径的效率,它是NEON处理交错格式BGR数据的标准流程。
2. 避免错误尝试:直接用vld3q_u16不可行
源数据是紧凑的8位字节排列,并非16位对齐的半字数组,强行用vld3q_u16加载会导致数据错位,后续还需要额外的修正操作,反而增加开销。
3. 大块数据加载+分摊开销
如果场景允许,建议加载更大批次的数据(比如16个像素,对应48字节),再拆分转换,以此分摊加载和转换的固定开销:
void foo(uint8_t* bgr, uint16_t width, uint16_t height) { for (uint16_t y = 0; y < height; y++) { uint16_t x; // 处理16像素的大块循环,提升吞吐量 for (x = 0; x <= width - 16; x += 16) { uint8x16x3_t bgrChunk = vld3q_u8(bgr); // 拆分低8位和高8位分别转换 uint16x8_t b_low = vmovl_u8(vget_low_u8(bgrChunk.val[0])); uint16x8_t b_high = vmovl_u8(vget_high_u8(bgrChunk.val[0])); uint16x8_t g_low = vmovl_u8(vget_low_u8(bgrChunk.val[1])); uint16x8_t g_high = vmovl_u8(vget_high_u8(bgrChunk.val[1])); uint16x8_t r_low = vmovl_u8(vget_low_u8(bgrChunk.val[2])); uint16x8_t r_high = vmovl_u8(vget_high_u8(bgrChunk.val[2])); // 对low和high批次的数据分别执行后续操作 bgr += 48; } // 处理剩余不足16像素的部分 for (; x < width; x += 8) { uint8x8x3_t bgrChunk = vld3_u8(bgr); uint16x8_t b = vmovl_u8(bgrChunk.val[0]); uint16x8_t g = vmovl_u8(bgrChunk.val[1]); uint16x8_t r = vmovl_u8(bgrChunk.val[2]); bgr += 24; // ... 处理剩余数据 } } }
4. 内存对齐优化
确保bgr指针是16字节对齐的(可以用__attribute__((aligned(16)))声明数组,或在内存分配时指定对齐),vld3_u8在对齐内存上的加载效率会显著提升,避免额外的对齐处理开销。
5. 编译器优化加持
编译时开启-O3和-march=armv8-a+simd(或更高版本如armv8.2-a+simd),编译器会自动重排指令序列,把vmovl_u8的转换操作和后续计算指令重叠执行,进一步隐藏延迟。
内容的提问来源于stack exchange,提问作者ImJustACowLol

