1. 概述
假设原始图像为1bit,即一个字节数据可以表示8个像素的状态[0,1],如下图所示,图像的前两个字节可以表示图像的16个像素信息。
现在需要将1bit图像转换为8bit图像,即将原始图像的每个字节拆分为8个字节,数据范围为[0,255]。如下图所示,将上图中的两个字节拆分为如下形式:
2. 代码实现
2.1. C 语言
上述功能可以通过C语言实现如下:
for (i = 0; i < IMG_SIZE; i++)
{
data_8bit[i] = ((data_1bit[i/8] >> (i % 8)) & 0x01) << 7;
}
运行时间大概为10.2
ms。
2.2. NEON加速
上述功能可以使用NEON进行加速,思路如下:
-
加载操作
使用VLD1
指令从内存空间加载图像数据到Q6
寄存器,该指令可以一次加载16个字节数据,对应图像的128个像素值;
-
位与操作
使用VAND
指令,对Q6
与Q4
寄存器的元素进行位与
操作,并将结果存入Q7
寄存器,其中Q4
寄存器可使用DUP
指令全部置为0x01
;此步骤将每个元素的低位像素值取出放入Q7
寄存器中;
-
比较操作
使用VCGT
指令对Q7
和Q5
寄存器的元素逐个进行比较,判断Q7
寄存器的元素是否大于对应Q5
中的元素,其计算结果保存在Q8
寄存器中。如果Q7
的元素大于Q5
的元素,则对应Q8
元素设置为255,否则设置为0。
-
移位操作
使用VSHR
指令,将Q6
中的元素整体向右移动1比特位,即将下一组像素移到元素最低位,方便循环取出图像像素值。
-
循环操作
循环上述2、3、4步骤,将Q6
寄存器中的像素值分别取出,放入Q8~Q15
寄存器中。
-
打包操作
使用VZIP
指令每隔4个Q
寄存器进行一次打包操作,如下图所示,使像素索引号间隔4排列。该步骤的目的是未了方便下一步的存储操作。
-
存储操作
使用VST4
指令将上述打包好的数据,按照像素循序存储相应的内存地址。
2.2.1. NEON Intrinsics
按照上述的算法思路,使用NEON Intrinsics编写上述C代码的功能,详细代码如下:
void NfsOneBitToU8(uint8_t* restrict input, uint8_t* restrict output, uint32_t size) {
// neon for speed
uint8x16_t v_zero = vdupq_n_u8(0x00);
uint8x16_t v_one = vdupq_n_u8(0x01);
uint8x16_t v_and_result;
uint8x16x4_t result_array1, result_array2;
uint8x16x4_t zip_out1, zip_out2;
for (int32_t i = 0; (i + 15) < size; i += 16) {
uint8x16_t v_input = vld1q_u8(input + i);
for (int j = 0; j < 4; j++) {
v_and_result = vandq_u8(v_input, v_one); // v & 0x01
result_array1.val[j] = vcgtq_u8(v_and_result, v_zero);
v_input = vshrq_n_u8(v_input, 1);
}
for (int j = 0; j < 4; j++) {
v_and_result = vandq_u8(v_input, v_one); // v & 0x01
result_array2.val[j] = vcgtq_u8(v_and_result, v_zero);
v_input = vshrq_n_u8(v_input, 1);
}
uint8x16x2_t zip_temp1 = vzipq_u8(result_array1.val[0], result_array2.val[0]);
uint8x16x2_t zip_temp2 = vzipq_u8(result_array1.val[1], result_array2.val[1]);
uint8x16x2_t zip_temp3 = vzipq_u8(result_array1.val[2], result_array2.val[2]);
uint8x16x2_t zip_temp4 = vzipq_u8(result_array1.val[3], result_array2.val[3]);
zip_out1.val[0] = zip_temp1.val[0];
zip_out1.val[1] = zip_temp2.val[0];
zip_out1.val[2] = zip_temp3.val[0];
zip_out1.val[3] = zip_temp4.val[0];
zip_out2.val[0] = zip_temp1.val[1];
zip_out2.val[1] = zip_temp2.val[1];
zip_out2.val[2] = zip_temp3.val[1];
zip_out2.val[3] = zip_temp4.val[1];
vst4q_u8(output + i * 8, zip_out1);
vst4q_u8(output + i * 8 + 64, zip_out2);
}
}
2.2.2 NEON Assembly
同理,使用NEON Assembly重新编写上述代码,详细如下:
void NfsOneBitToU8Asm(uint8_t* restrict input, uint8_t* restrict output, uint32_t size) {
uint8_t* input_temp = input;
uint8_t* output_temp = output;
uint32_t size_tmp = size;
asm volatile("vmov.i8 q4, #0x01 \n"
"vmov.i8 q5, #0x00 \n"
"100: \n"
"vld1.8 {q6}, [%0]! \n"
"pld [%0, #128] \n"
"vshr.u8 q9, q6, #1 \n"
"vshr.u8 q10, q6, #2 \n"
"vshr.u8 q11, q6, #3 \n"
"vshr.u8 q12, q6, #4 \n"
"vshr.u8 q13, q6, #5 \n"
"vshr.u8 q14, q6, #6 \n"
"vshr.u8 q15, q6, #7 \n"
"vand.i8 q8, q6, q4 \n"
"vand.i8 q9, q9, q4 \n"
"vand.i8 q10, q10, q4 \n"
"vand.i8 q11, q11, q4 \n"
"vand.i8 q12, q12, q4 \n"
"vand.i8 q13, q13, q4 \n"
"vand.i8 q14, q14, q4 \n"
"vand.i8 q15, q15, q4 \n"
"vcgt.s8 q8, q8, q5 \n"
"vcgt.s8 q9, q9, q5 \n"
"vcgt.s8 q10, q10, q5 \n"
"vcgt.s8 q11, q11, q5 \n"
"vcgt.s8 q12, q12, q5 \n"
"vcgt.s8 q13, q13, q5 \n"
"vcgt.s8 q14, q14, q5 \n"
"vcgt.s8 q15, q15, q5 \n"
"vzip.8 q8, q12 \n"
"vzip.8 q9, q13 \n"
"vzip.8 q10, q14 \n"
"vzip.8 q11, q15 \n"
"vst4.8 {d16, d18, d20, d22}, [%1]! \n"
"vst4.8 {d17, d19, d21, d23}, [%1]! \n"
"vst4.8 {d24, d26, d28, d30}, [%1]! \n"
"vst4.8 {d25, d27, d29, d31}, [%1]! \n"
"subs %2, #16 \n"
"bgt 100b \n"
: "=r"(input_temp), "=r"(output_temp), "=r"(size_tmp)
: "0"(input_temp), "1"(output_temp), "2"(size_tmp)
: "memory",
"cc",
"q4",
"q5",
"q6",
"q7",
"q8",
"q9",
"q10",
"q11",
"q12",
"q13",
"q14",
"q15");
}
2.3.性能对比
如下表所示,上述代码分别在某A9平台运行性能如下:
Function | C language | Intrinsics | Assembly |
---|---|---|---|
NfsOneBitToU8 | 10.2 ms | 2.13 ms | 1.78 ms |
3. 结论
通过上述列子可以看出,使用NEON对算法进行加速效果明显,使用NEON Intrinsics已经提升了很多性能,而且该方式方便移植,编译器也会对指令进行重排。当然为了进一步提升性能可以使用汇编语言编写上述NEON代码,性能可以进一步提升,但提升的程度相对较小。