1、前言
在上個博客中對NEON有了基礎的了解,本文將針對一個圖像下采樣的例子對NEON進行學習。
學習鏈接:CPU優化技術 - NEON 開發進階
上文鏈接:https://blog.csdn.net/weixin_42108183/article/details/136412104
2、第一個例子
現在有一張圖片,需要對UV通道的數據進行下采樣,對于同種類型的數據,相鄰的4個元素求和并求均值。示意圖如下圖所示:
假定圖像數據的寬為16的整數倍,如果使用c++代碼,可以寫出下面的代碼:
void DownscaleUv(uint8_t *src, uint8_t *dst, int32_t src_stride, int32_t dst_width, int32_t dst_height, int32_t dst_stride)
{//遍歷每一行的數據for (int32_t j = 0; j < dst_height; j++){ // 偶數行起始位置,uint8_t *src_ptr0 = src + src_stride * j * 2;// 奇數行起始位置uint8_t *src_ptr1 = src_ptr0 + src_stride;// 存儲起始位置uint8_t *dst_ptr = dst + dst_stride * j;// 沒一次循環計算沒for (int32_t i = 0; i < dst_width; i += 2){// U通道 (u1 + u2 + u3 + u4) / 4dst_ptr[i] = (src_ptr0[i * 2] + src_ptr0[i * 2 + 2] +src_ptr1[i * 2] + src_ptr1[i * 2 + 2]) / 4;// V通道 (v1 + v2 + v3 + v4) / 4dst_ptr[i + 1] = (src_ptr0[i * 2 + 1] + src_ptr0[i * 2 + 3] +src_ptr1[i * 2 + 1] + src_ptr1[i * 2 + 3]) / 4;}}
}
通過學習向量化編程,我們可知,數據的計算可以利用單指令多數據的方式進行加速,例如上面的例子中的內層循環,下面就使用NEON來試試吧。
3、第2個例子
為了進行向量化加速,首先需要將UV數據分離,將UV數據分離的操作在NEON中很容易進行, 使用vld2交織加載或者儲存即可。對于每一行的數據,交織加載的示意圖如下。
交織加載的基本原理是按照間隔挑選數據。交織加載的例子如下所示:
void DownscaleUvNeon()
{vector<uint8_t> data; // UVUVUVUVUV...for(int i=0;i<32;i++){data.push_back(i);}// uint8_t *src_ptr0 = (uint8_t *)data.data(); // load 第一行的數據uint8x16x2_t src;src = vld2q_u8(src_ptr0); // 交織讀取 16 * 2 的數據,需要兩個q寄存器。auto a = src_odd.val[0]; // 一行的U數據vector<uint8_t> show_data(16);vst1q_u8 (show_data.data(),a); // 將U數據順序儲存到內存中// 打印for(auto n : show_data){cout << static_cast<int>(n) << endl; // 0,2,4,6,...}
}
4、第3個例子
對于下UV數據采樣來說,在偶數行進行上面的交織加載,再在奇數行上進行同樣的操作。奇數行和偶數行相應的數據進行相加再求平均,即可得到最后的結果。代碼實現如下:
#include <arm_neon.h>
void DownscaleUvNeon(uint8_t *src, uint8_t *dst, int32_t src_width, int32_t src_stride, int32_t dst_width, int32_t dst_height, int32_t dst_stride)
{//用于加載偶數行的源數據,2組每組16個u8類型數據,(16 * 8) * 2 = 128 * 128, 因此需要兩個q寄存器。 uint8x16x2_t v8_src0;//用于加載奇數行的源數據uint8x16x2_t v8_src1;//目的數據變量,需要一個Q寄存器uint8x8x2_t v8_dst;//目前只處理16整數倍部分的結果int32_t dst_width_align = dst_width & (-16); // dst_width & (-16),最大能夠整除16的數。//向量化剩余的部分需要單獨處理int32_t remain = dst_width & 15;int32_t i = 0;//外層高度循環,逐行處理for (int32_t j = 0; j < dst_height; j++){//偶數行源數據地址uint8_t *src_ptr0 = src + src_stride * j * 2;//奇數行源數據地址uint8_t *src_ptr1 = src_ptr0 + src_stride;//目的數據指針uint8_t *dst_ptr = dst + dst_stride * j;//內層循環,一次16個u8結果輸出for (i = 0; i < dst_width_align; i += 16){//提取數據,進行UV分離v8_src0 = vld2q_u8(src_ptr0); src_ptr0 += 32; // 偶數行進入下一個stridev8_src1 = vld2q_u8(src_ptr1);src_ptr1 += 32; // 奇數行行進入下一個stride//水平兩個數據相加uint16x8_t v16_u_sum0 = vpaddlq_u8(v8_src0.val[0]);uint16x8_t v16_v_sum0 = vpaddlq_u8(v8_src0.val[1]);uint16x8_t v16_u_sum1 = vpaddlq_u8(v8_src1.val[0]);uint16x8_t v16_v_sum1 = vpaddlq_u8(v8_src1.val[1]);//上下兩個數據相加,之后求均值v8_dst.val[0] = vshrn_n_u16(vaddq_u16(v16_u_sum0, v16_u_sum1), 2);v8_dst.val[1] = vshrn_n_u16(vaddq_u16(v16_v_sum0, v16_v_sum1), 2);//UV通道結果交織存儲vst2_u8(dst_ptr, v8_dst);dst_ptr += 16;}//process leftovers......}
}
5、第4個例子
當圖像的寬度不是16的整數倍,需要考慮結尾數據處理,按照鏈接里面的例子,可以分為以下幾種。
1、 padding
? 也就是將數據補齊到想要的長度,如下圖所示,比如我這里需要操作 uint8x8_t的數據,但是我的數據長度只有5,可以將數據的長度填充至8。
2、Overlap
? 也就是重復利用其中的某些數據,在不填充其他數據的情況下進行,如下圖所示,當需要利用uint8x4_t來對下面的數據進行計算時,可以先將04加載到寄存器上,再將36加載到寄存器上操作。
常用第二種方法對結尾數據進行處理,那么圖像下采樣的數據代碼可以寫成:
#include <arm_neon.h>void DownscaleUvNeon(uint8_t *src, uint8_t *dst, int32_t src_width, int32_t src_stride, int32_t dst_width, int32_t dst_height, int32_t dst_stride)
{uint8x16x2_t v8_src0;uint8x16x2_t v8_src1;uint8x8x2_t v8_dst;int32_t dst_width_align = dst_width & (-16); // 最大能夠整除16的數。int32_t remain = dst_width & 15; // 需要剩余處理的數據長度int32_t i = 0;for (int32_t j = 0; j < dst_height; j++){uint8_t *src_ptr0 = src + src_stride * j * 2;uint8_t *src_ptr1 = src_ptr0 + src_stride;uint8_t *dst_ptr = dst + dst_stride * j;// 處理完寬度為16的整數倍數據了for (i = 0; i < dst_width_align; i += 16){v8_src0 = vld2q_u8(src_ptr0);src_ptr0 += 32;v8_src1 = vld2q_u8(src_ptr1);src_ptr1 += 32;uint16x8_t v16_u_sum0 = vpaddlq_u8(v8_src0.val[0]);uint16x8_t v16_v_sum0 = vpaddlq_u8(v8_src0.val[1]);uint16x8_t v16_u_sum1 = vpaddlq_u8(v8_src1.val[0]);uint16x8_t v16_v_sum1 = vpaddlq_u8(v8_src1.val[1]);v8_dst.val[0] = vshrn_n_u16(vaddq_u16(v16_u_sum0, v16_u_sum1), 2);v8_dst.val[1] = vshrn_n_u16(vaddq_u16(v16_v_sum0, v16_v_sum1), 2);vst2_u8(dst_ptr, v8_dst);dst_ptr += 16;}// process leftover// remain 剩余需要處理的數據長度if (remain > 0){// 從后往前回退一次向量計算需要的數據長度// 有部分數據是之前處理過的,這部分的數據在這里重復計算一次src_ptr0 = src + src_stride * (j * 2) + src_width - 32; src_ptr1 = src_ptr0 + src_stride;dst_ptr = dst + dst_stride * j + dst_width - 16;v8_src0 = vld2q_u8(src_ptr0);v8_src1 = vld2q_u8(src_ptr1);uint16x8_t v16_u_sum0 = vpaddlq_u8(v8_src0.val[0]);uint16x8_t v16_v_sum0 = vpaddlq_u8(v8_src0.val[1]);uint16x8_t v16_u_sum1 = vpaddlq_u8(v8_src1.val[0]);uint16x8_t v16_v_sum1 = vpaddlq_u8(v8_src1.val[1]);v8_dst.val[0] = vshrn_n_u16(vaddq_u16(v16_u_sum0, v16_u_sum1), 2);v8_dst.val[1] = vshrn_n_u16(vaddq_u16(v16_v_sum0, v16_v_sum1), 2);vst2_u8(dst_ptr, v8_dst);}}
}
3、 single
將剩余的元素單獨處理,就是將剩余的元素利用NEON的只加載一個元素的功能,不推薦使用,因為這里又可能for循環多次。
4、將剩余的元素當作標量處理
也就是將剩下的元素直接使用c語言編程的方式進行計算。
void DownscaleUvNeonScalar(uint8_t *src, uint8_t *dst, int32_t src_width, int32_t src_stride, int32_t dst_width, int32_t dst_height, int32_t dst_stride)
{uint8x16x2_t v8_src0;uint8x16x2_t v8_src1;uint8x8x2_t v8_dst;int32_t dst_width_align = dst_width & (-16);int32_t remain = dst_width & 15;int32_t i = 0;for (int32_t j = 0; j < dst_height; j++){uint8_t *src_ptr0 = src + src_stride * j * 2;uint8_t *src_ptr1 = src_ptr0 + src_stride;uint8_t *dst_ptr = dst + dst_stride * j;for (i = 0; i < dst_width_align; i += 16) // 16 items output at one time{v8_src0 = vld2q_u8(src_ptr0);src_ptr0 += 32;v8_src1 = vld2q_u8(src_ptr1);src_ptr1 += 32;uint16x8_t v16_u_sum0 = vpaddlq_u8(v8_src0.val[0]);uint16x8_t v16_v_sum0 = vpaddlq_u8(v8_src0.val[1]);uint16x8_t v16_u_sum1 = vpaddlq_u8(v8_src1.val[0]);uint16x8_t v16_v_sum1 = vpaddlq_u8(v8_src1.val[1]);v8_dst.val[0] = vshrn_n_u16(vaddq_u16(v16_u_sum0, v16_u_sum1), 2);v8_dst.val[1] = vshrn_n_u16(vaddq_u16(v16_v_sum0, v16_v_sum1), 2);vst2_u8(dst_ptr, v8_dst);dst_ptr += 16;}//process leftoversrc_ptr0 = src + src_stride * j * 2;src_ptr1 = src_ptr0 + src_stride;dst_ptr = dst + dst_stride * j;for (int32_t i = dst_width_align; i < dst_width; i += 2){dst_ptr[i] = (src_ptr0[i * 2] + src_ptr0[i * 2 + 2] +src_ptr1[i * 2] + src_ptr1[i * 2 + 2]) / 4;dst_ptr[i + 1] = (src_ptr0[i * 2 + 1] + src_ptr0[i * 2 + 3] +src_ptr1[i * 2 + 1] + src_ptr1[i * 2 + 3]) / 4;}}
}
6、總結
本次學習中通過一個下采樣的例子學習的NEON編程過程中的優勢以及將要面臨的問題,主要是剩余數據處理的方式,后面將繼續深入學習。
/ 4;
dst_ptr[i + 1] = (src_ptr0[i * 2 + 1] + src_ptr0[i * 2 + 3] +src_ptr1[i * 2 + 1] + src_ptr1[i * 2 + 3]) / 4;}
}
}
#### 6、總結本次學習中通過一個下采樣的例子學習的NEON編程過程中的優勢以及將要面臨的問題,主要是剩余數據處理的方式,后面將繼續深入學習。