一、概述
在前面的"CPU 優(yōu)化技術(shù)"系列文章中我們對(duì)NEON做了系統(tǒng)的介紹和說(shuō)明,包括SIMD和NEON概念,NEON自動(dòng)向量化以及NEON intrinsic指令集等。但是只掌握這些還不足以編寫一個(gè)性能完善的NEON程序,在實(shí)際的NEON優(yōu)化工作中我們會(huì)遇到如何將標(biāo)量處理轉(zhuǎn)換為向量處理,如何更高效的處理圖像的邊界區(qū)域等問(wèn)題。接下來(lái)我們會(huì)針這些問(wèn)題進(jìn)行介紹和說(shuō)明,讓大家可以在實(shí)際工作中使用NEON來(lái)優(yōu)化程序的性能。
本文我們會(huì)介紹代碼如何進(jìn)行向量化,如何處理向量化的剩余部分,如何處理圖像的邊界區(qū)域,最后會(huì)給出一個(gè)完整的NEON程序?qū)嵗?/p>
二、概述向量化編程
2.1.?向量化
向量化就是使用SIMD指令同時(shí)對(duì)多個(gè)數(shù)據(jù)進(jìn)行處理,達(dá)到提升程序性能的目的。
我們以數(shù)據(jù)加法為例,標(biāo)量和向量處理的對(duì)比圖如下。對(duì)于無(wú)符號(hào)16位類型的加法運(yùn)算,普通的標(biāo)量加法需要進(jìn)行8次的計(jì)算量,使用向量加法指令一次就可以完成。
相比于標(biāo)量編程,向量化編程對(duì)于初學(xué)者來(lái)說(shuō)有一定的難度:
編程方式的變化:一次處理的不再是單個(gè)數(shù)據(jù)而是多個(gè)數(shù)據(jù),同時(shí)還要專門處理向量化的剩余數(shù)據(jù)。
向量數(shù)據(jù)類型的選擇:要根據(jù)實(shí)際的情況選擇最合適的向量寄存器。
選擇合適的指令:需要非常熟悉NEON指令集,使用最適合的指令獲得最好的性能。
2.2 實(shí)例講解
這是一個(gè)UV通道下采樣代碼,輸入是u8類型的數(shù)據(jù),通過(guò)鄰近的4個(gè)像素求平均,輸出u8類型的數(shù)據(jù),達(dá)到1/4下采樣的目的。我們假定每行數(shù)據(jù)長(zhǎng)度是16的整數(shù)倍。算法的示意圖和參考代碼如下所示。
C代碼實(shí)現(xiàn):
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通道 dst_ptr[i] = (src_ptr0[i * 2] + src_ptr0[i * 2 + 2] + src_ptr1[i * 2] + src_ptr1[i * 2 + 2]) / 4; // V通道 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; } } }
2.2.1 內(nèi)層循環(huán)向量化
內(nèi)層循環(huán)是代碼執(zhí)行次數(shù)最多的部分,因此是向量化的重點(diǎn)。我們的輸入和輸出都是u8類型,NEON寄存器128bit,所以我們每次處理16個(gè)數(shù)據(jù)。
// 每次有16個(gè)數(shù)據(jù)輸出
for (i = 0; i < dst_width; i += 16) { //數(shù)據(jù)處理部分...... }
2.2.2 數(shù)據(jù)類型的選擇
2.2.3 指令的選擇
輸入數(shù)據(jù)加載:UV通道的數(shù)據(jù)是交織的,使用vld2指令可以實(shí)現(xiàn)解交織。
2.2.4 代碼實(shí)現(xiàn)
//使用intrinsic需要包含的頭文件 #includevoid 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) { //load偶數(shù)行的源數(shù)據(jù),2組每組16個(gè)u8類型數(shù)據(jù) uint8x16x2_t v8_src0; //load奇數(shù)行的源數(shù)據(jù),需要兩個(gè)Q寄存器 uint8x16x2_t v8_src1; //目的數(shù)據(jù)變量,需要一個(gè)Q寄存器 uint8x8x2_t v8_dst; //目前只處理16整數(shù)倍部分的結(jié)果 int32_t dst_width_align = dst_width & (-16); //向量化剩余的部分需要單獨(dú)處理 int32_t remain = dst_width & 15; int32_t i = 0; //外層高度循環(huán),逐行處理 for (int32_t j = 0; j < dst_height; j++) { //偶數(shù)行源數(shù)據(jù)指針 uint8_t *src_ptr0 = src + src_stride * j * 2; //奇數(shù)行源數(shù)據(jù)指針 uint8_t *src_ptr1 = src_ptr0 + src_stride; //目的數(shù)據(jù)指針 uint8_t *dst_ptr = dst + dst_stride * j; //內(nèi)層循環(huán),一次16個(gè)u8結(jié)果輸出 for (i = 0; i < dst_width_align; i += 16) { //提取數(shù)據(jù),進(jìn)行UV分離 v8_src0 = vld2q_u8(src_ptr0); src_ptr0 += 32; v8_src1 = vld2q_u8(src_ptr1); src_ptr1 += 32; //水平兩個(gè)數(shù)據(jù)相加 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]); //上下兩個(gè)數(shù)據(jù)相加,之后求均值 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通道結(jié)果交織存儲(chǔ) vst2_u8(dst_ptr, v8_dst); dst_ptr += 16; } //process leftovers...... } }
2.3 向量化剩余部分(leftovers)處理
接著上面的實(shí)例,內(nèi)層循環(huán)每次計(jì)算16個(gè)結(jié)果,當(dāng)輸出圖像寬度不是16整數(shù)倍的時(shí)候,我們需要考慮結(jié)尾如何高效的編寫?!癗EON Programmer's Guide”中給出了幾種推薦寫法,下面逐一介紹一下。
2.3.1 Extend arrays with padding
這個(gè)方法比較好理解,每行數(shù)據(jù)長(zhǎng)度不是向量長(zhǎng)度整數(shù)倍我們可以提前將數(shù)據(jù)補(bǔ)齊到需要的長(zhǎng)度,這樣處理時(shí)候就方便了。這個(gè)方法的使用是要分情況的。
如果需要自己申請(qǐng)內(nèi)存,復(fù)制來(lái)擴(kuò)展邊界,這并不是一種高效的方法。
如果外部數(shù)據(jù)先要經(jīng)過(guò)其他的處理(例如rgb2yuv),我們可以考慮將前一級(jí)的輸出保存成需要的長(zhǎng)度,這樣后面的uv下采樣就可以得到擴(kuò)展的內(nèi)存了。
2.3.2 Overlap data elements
這種做法是在處理尾部數(shù)據(jù)的時(shí)候,從后往前提取一個(gè)向量的數(shù)據(jù)進(jìn)行計(jì)算,這樣會(huì)出現(xiàn)一部分?jǐn)?shù)據(jù)重復(fù)計(jì)算。接著2.2.4節(jié)的示例,這種方法的實(shí)現(xiàn)代碼如下:
#includevoid 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); 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) { 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 if (remain > 0) { //從后往前回退一次向量計(jì)算需要的數(shù)據(jù)長(zhǎng)度,有部分?jǐn)?shù)據(jù)是之前處理過(guò)的 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); } } }
以上這種方法我們平時(shí)用的比較多,不僅可以處理剩余元素,而且可以保持向量處理的高效性。
2.3.3 Process leftovers as single elements
這種做法利用NEON向量可以只加載/存儲(chǔ)一個(gè)元素的功能,雖然使用向量指令,但是每個(gè)結(jié)果獨(dú)立計(jì)算和存儲(chǔ)。這是一種很不推薦的方法。每次的向量計(jì)算只使用一個(gè)元素,浪費(fèi)了計(jì)算資源(NEON指令相比于標(biāo)量指令的執(zhí)行周期要長(zhǎng),各指令執(zhí)行時(shí)間可以參考文獻(xiàn)[2])。
2.3.4 標(biāo)量處理剩余部分
剩余部分直接采用標(biāo)量來(lái)處理,這種是最簡(jiǎn)單的方法,也是最常用的方法,每行的剩余元素可以簡(jiǎn)單的用標(biāo)量處理,因?yàn)榻^大部分都是向量計(jì)算,剩余元素所占比例非常小,因此使用標(biāo)量不會(huì)對(duì)性能產(chǎn)生太明顯的影響。
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 leftover src_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; } } }
三、邊界處理方法
在許多圖像處理算法中,經(jīng)常會(huì)遇到需要處理邊界的情況。例如灰度圖的3x3高斯濾波,為了計(jì)算邊界附近點(diǎn)的輸出,需要在原圖的上下左右各填充1個(gè)像素的padding。
一種通用的處理方法是申請(qǐng)一塊添加了邊界大小的內(nèi)存空間,將邊界填充為需要的數(shù)據(jù),并且將原有數(shù)據(jù)復(fù)制到新申請(qǐng)的內(nèi)存空間中,完成擴(kuò)邊操作(openCV采用的就是這種做法)。這樣新的數(shù)據(jù)塊中就有了邊界數(shù)據(jù),后面的數(shù)據(jù)處理就很方便了。
但是通用方法不一定是最優(yōu)的方法,內(nèi)存申請(qǐng)和填充會(huì)增加大量的額外時(shí)間,對(duì)提升算法性能很不利。我們可以充分利用NEON指令在幾乎不增加時(shí)間空間開(kāi)銷的前提下完成一些特殊的邊界處理。
3.1 常量填充
常量填充就是在有效數(shù)據(jù)塊的上下左右添加常量邊界值,完成數(shù)據(jù)的擴(kuò)充。例如3x3高斯濾波計(jì)算需要在上下左右添加1個(gè)常量邊界值進(jìn)行計(jì)算。
上下邊界的填充比較簡(jiǎn)單,我們只需要使用vdup指令填充一個(gè)向量v8_pre_row_data。
左右邊界填充也需要用到dup來(lái)的向量v8_const_pad,使用vext來(lái)組建新的向量,示意圖及參考代碼如下。
//dup指令生成pading向量 uint8x16_t v8_const_pad = vdupq_n_u8(pad_val); //-1行數(shù)據(jù) v8_pre_row_data = v8_const_pad; //讀取第0行數(shù)據(jù) uint8x16_t v8_tmp_data = vld1q_u8(pt_row0); //第0行帶有左padding的數(shù)據(jù) uint8x16_t v8_row_cur_data = vextq_u8(v8_const_pad, v8_tmp_data, 15); //讀取第1行數(shù)據(jù) v8_tmp_data = vld1q_u8(pt_row1); //第1行帶有左padding的數(shù)據(jù) uint8x16_t v8_next_row_data = vextq_u8(v8_const_pad, v8_tmp_data, 15);
3.2 復(fù)制填充
復(fù)制填充就是復(fù)制最邊緣的像素作為邊界。我們同樣以3x3高斯濾波計(jì)算為例。
上下邊界的方法一樣,我們可以使用vld加載第0行或者最后一行的數(shù)據(jù)即可。
左右邊界的方法一樣,對(duì)于左邊界,我們可以使用VLD1_DUP指令提取邊界數(shù)據(jù),然后使用vext來(lái)組建新的向量,參考代碼如下。
//提取0行padding數(shù)據(jù) uint8x16_t v8_dup_pad = vld1q_dup_u8(pt_row0); //提取第0行數(shù)據(jù) uint8x16_t v8_tmp_data = vld1q_u8(pt_row0); //第0行帶有左padding的數(shù)據(jù) uint8x16_t v8_row_cur_data = vextq_u8(v8_dup_pad, v8_tmp_data, 15); //-1行直接使用第0行 uint8x16_t v8_pre_row_data = v8_row_cur_data; //取1行padding數(shù)據(jù) v8_dup_pad = vld1q_dup_u8(pt_row1); v8_tmp_data = vld1q_u8(pt_row1); //第1行帶有左padding的數(shù)據(jù) uint8x16_t v8_next_row_data = vextq_u8(v8_dup_pad, v8_tmp_data, 15);
3.3 反射填充
常見(jiàn)的有反射(dcba"abcdefgh"hgfed)和101反射(edcb"abcdefgh"gfed),處理的方式幾乎一樣,我們以稍復(fù)雜的101反射介紹,同樣選擇3x3高斯濾波計(jì)算舉例。
上下邊界的方法一樣,我們需要根據(jù)反射類型,將padding行的數(shù)據(jù)向量賦值為相應(yīng)行的數(shù)據(jù)向量即可。左右邊界的方法一樣,對(duì)于左邊界,我們可以使用VLD1指令提取邊界數(shù)據(jù),然后使用vrev來(lái)翻轉(zhuǎn)向量?jī)?nèi)部元素最后使用vext來(lái)組建新的向量。
參考代碼:
uint8x8_t v8_ref_pad = vld1_u8(pt_row0 + 1); uint8x8_t v8_ref_pad1; uint8x8_t v8_tmp_data = vld1q_u8(pt_row0); //翻轉(zhuǎn)數(shù)據(jù),用于生成101反射padding v8_ref_pad1 = vrev64_u8(v8_ref_pad); //第0行帶有左padding的數(shù)據(jù) uint8x8_t v8_cur_row_data = vextq_u8(vcombine_u8(v8_ref_pad, v8_ref_pad1), v8_tmp_data, 15); v8_ref_pad = vld1_u8(pt_row1 + 1); v8_tmp_data = vld1q_u8(pt_row1); v8_ref_pad1 = vrev64_u8(v8_ref_pad); //第1行帶有左padding的數(shù)據(jù) uint8x8_t v8_next_row_data = vextq_u8(vcombine_u8(v8_ref_pad, v8_ref_pad1), v8_tmp_data, 15); //-1行數(shù)據(jù) uint8x8_t v8_pre_row_data = v8_next_row_data;
四、優(yōu)化實(shí)例
4.1 說(shuō)明
我們使用核參數(shù)為{{1,2,1},{2,4,2},{1,2,1}}對(duì)灰度圖(size:4095x2161)做高斯濾波,邊界填充類型為BORDER_REFLECT101。
4.2 過(guò)程分析
整體流程:
Gaussian3x3Sigma0NeonU8C1是主函數(shù)
Gaussian3x3RowCalcu是行處理函數(shù),完成一行的處理
第一次處理上邊邊界,然后是中間處理,最后是下邊界處理
int32_t Gaussian3x3Sigma0NeonU8C1(const uint8_t *src, uint8_t *dst, int32_t height, int32_t width, int32_t istride, int32_t ostride) { if ((NULL == src) || (NULL == dst)) { printf("input param invalid! "); return -1; } //BORDER_REFLECT101 top padding const uint8_t *p_src0 = src + istride; const uint8_t *p_src1 = src; const uint8_t *p_src2 = src + istride; uint8_t *p_dst = dst; //計(jì)算第0行輸出 Gaussian3x3RowCalcu(p_src0, p_src1, p_src2, p_dst, width); //中間行的處理 for (int32_t row = 1; row < height - 1; row++) { p_src0 = src + (row - 1) * istride; p_src1 = src + (row - 0) * istride; p_src2 = src + (row + 1) * istride; p_dst = dst + row * ostride; Gaussian3x3RowCalcu(p_src0, p_src1, p_src2, p_dst, width); } //計(jì)算最后一行輸出 p_src0 = src + (height - 2) * istride; p_src1 = src + (height - 1) * istride; p_src2 = src + (height - 2) * istride; p_dst = dst + (height - 1) * ostride; Gaussian3x3RowCalcu(p_src0, p_src1, p_src2, p_dst, width); return 0; }
Gaussian3x3RowCalcu實(shí)現(xiàn)
內(nèi)聯(lián)函數(shù),完成一行的處理,基于高斯行列分離計(jì)算,先計(jì)算行累加,然后計(jì)算列累加。
左邊界處理:
static inline int32_t Gaussian3x3RowCalcu(const uint8_t *src0, const uint8_t *src1, const uint8_t *src2, uint8_t *dst, int32_t width) { if ((NULL == src0) || (NULL == src1) || (NULL == src2) || (NULL == dst)) { printf("input param invalid! "); return -1; } int32_t col = 0; uint16x8_t vqn0, vqn1, vs_1, vs, vs1; uint8x8_t v_lnp; int32_t width_t = (width - 9) & (-8); uint8x8_t v_ld00 = vld1_u8(src0); uint8x8_t v_ld01 = vld1_u8(src0 + 8); uint8x8_t v_ld10 = vld1_u8(src1); uint8x8_t v_ld11 = vld1_u8(src1 + 8); uint8x8_t v_ld20 = vld1_u8(src2); uint8x8_t v_ld21 = vld1_u8(src2 + 8); //豎直方向3行的累加和 vqn0 = vaddl_u8(v_ld00, v_ld20); vqn0 = vaddq_u16(vqn0, vshll_n_u8(v_ld10, 1)); vqn1 = vaddl_u8(v_ld01, v_ld21); vqn1 = vaddq_u16(vqn1, vshll_n_u8(v_ld11, 1)); //生成padding數(shù)據(jù) vs_1 = vextq_u16(vextq_u16(vqn0, vqn0, 2), vqn0, 7); vs1 = vextq_u16(vqn0, vqn1, 1); //水平方向累加和 vs = vaddq_u16(vaddq_u16(vqn0, vqn0), vaddq_u16(vs_1, vs1)); v_lnp = vqrshrn_n_u16(vs, 4); vst1_u8(dst, v_lnp); vs_1 = vextq_u16(vqn0, vqn1, 7); // for循環(huán)...... }
中間部分處理
第二部分for循環(huán)是計(jì)算中間部分?jǐn)?shù)據(jù)的結(jié)果,先做豎直方向的累加,再做水平方向的累加,每次計(jì)算8個(gè)輸出結(jié)果。各向量的數(shù)據(jù)含義及計(jì)算方法(for循環(huán)第一次計(jì)算)見(jiàn)下圖。
最后一次的向量計(jì)算單獨(dú)處理,為了防止提取下一組數(shù)據(jù)時(shí)越界。
static inline int32_t Gaussian3x3RowCalcu(const uint8_t *src0, const uint8_t *src1, const uint8_t *src2, uint8_t *dst, int32_t width) { // 計(jì)算前8個(gè)輸出...... for (col = 8; col < width_t; col += 8) { // 3行的輸入數(shù)據(jù) uint8x8_t v_ld0 = vld1_u8(src0 + col + 8); uint8x8_t v_ld1 = vld1_u8(src1 + col + 8); uint8x8_t v_ld2 = vld1_u8(src2 + col + 8); //豎直方向的累加和 uint16x8_t vqn2 = vaddl_u8(v_ld0, v_ld2); vqn2 = vaddq_u16(vqn2, vshll_n_u8(v_ld1, 1)); //水平方向累加和 vs1 = vextq_u16(vqn1, vqn2, 1); uint16x8_t vtmp = vshlq_n_u16(vqn1, 1); uint16x8_t v_sum = vaddq_u16(vtmp, vaddq_u16(vs1, vs_1)); uint8x8_t v_rst = vqrshrn_n_u16(v_sum, 4); vst1_u8(dst + col, v_rst); vs_1 = vextq_u16(vqn1, vqn2, 7); vqn1 = vqn2; } //最后一組向量計(jì)算,為了防止越界讀取數(shù)據(jù),右側(cè)數(shù)據(jù)只讀取一個(gè) { uint8x8_t v_ld0 = vld1_lane_u8(src0 + col + 8, v_ld0, 0); uint8x8_t v_ld1 = vld1_lane_u8(src1 + col + 8, v_ld1, 0); uint8x8_t v_ld2 = vld1_lane_u8(src2 + col + 8, v_ld2, 0); uint16x8_t vqn2 = vaddl_u8(v_ld0, v_ld2); vqn2 = vaddq_u16(vqn2, vshll_n_u8(v_ld1, 1)); vs1 = vextq_u16(vqn1, vqn2, 1); uint16x8_t vtmp = vshlq_n_u16(vqn1, 1); uint16x8_t v_sum = vaddq_u16(vtmp, vaddq_u16(vs1, vs_1)); uint8x8_t v_rst = vqrshrn_n_u16(v_sum, 4); vst1_u8(dst + col, v_rst); col += 8; } //process leftovers... }
最后剩余的非對(duì)齊部分我們使用標(biāo)量進(jìn)行計(jì)算。
static inline int32_t Gaussian3x3RowCalcu(const uint8_t *src0, const uint8_t *src1, const uint8_t *src2, uint8_t *dst, int32_t width) { // 向量計(jì)算部分...... for (; col < width; col++) { int32_t idx_l = (col == width - 1) ? width - 2 : col - 1; int32_t idx_r = (col == width - 1) ? width - 2 : col + 1; int32_t acc = 0; acc += (src0[idx_l] + src0[idx_r]); acc += (src0[col] << 1); acc += (src1[idx_l] + src1[idx_r]) << 1; acc += (src1[col] << 2); acc += (src2[idx_l] + src2[idx_r]); acc += (src2[col] << 1); uint16_t res = ((acc + (1 << 3)) >> 4) & 0xFFFF; dst[col] = CAST_U8(res); } return 0; }
4.3 運(yùn)行結(jié)果
下圖是我們?cè)?a href="http://m.hljzzgx.com/tags/高通/" target="_blank">高通驍龍888平臺(tái)上的運(yùn)行結(jié)果,可以看到使用NEON優(yōu)化之后運(yùn)行時(shí)間從15.53ms下降到了3.22ms,性能有了4倍多的提升。感興趣的讀者可以自己運(yùn)行下結(jié)果。
4.4 工程代碼?https://github.com/mobile-algorithm-optimization/guide/tree/main/NeonGaussian
編輯:黃飛
評(píng)論
查看更多