ARM NEON:将每像素 8 位二进制图像(仅 0/1)转换为每像素 1 位?
ARM NEON: Convert a binary 8-bit-per-pixel image (only 0/1) to 1-bit-per-pixel?
我正在执行一项任务,将每个像素有 8 位 (uint8_t
) 且每个像素只能为 0 或 1(或 255)的大型二进制标签图像转换为一个数组uint64_t
个数字,uint64_t
个数字中的每一位代表一个标签像素。
例如,
输入数组:0 1 1 0 ... (00000000 00000001 00000001 00000000 ...)
或输入数组:0 255 255 0 ... (00000000 11111111 11111111 00000000 ...)
输出数组(number):6
(因为每个uint8_t
转换成bit后变成0110
)
目前实现这个的C代码是:
for (int j = 0; j < width >> 6; j++) {
uint8_t* in_ptr= in + (j << 6);
uint64_t out_bits = 0;
if (in_ptr[0]) out_bits |= 0x0000000000000001;
if (in_ptr[1]) out_bits |= 0x0000000000000002;
.
.
.
if (in_ptr[63]) out_bits |= 0x8000000000000000;
*output = obits; output ++;
}
ARM NEON 能否优化此功能?请帮忙。谢谢!
假设输入值为 0 或 255,下面是基本版本,非常简单,特别适合有英特尔 SSE/AVX 经验的人。
void foo_basic(uint8_t *pDst, uint8_t *pSrc, intptr_t length)
{
//assert(length >= 64);
//assert(length & 7 == 0);
uint8x16_t in0, in1, in2, in3;
uint8x8_t out;
const uint8x16_t mask = {1, 2, 4, 8, 16, 32, 64, 128, 1, 2, 4, 8, 16, 32, 64, 128};
length -= 64;
do {
do {
in0 = vld1q_u8(pSrc); pSrc += 16;
in1 = vld1q_u8(pSrc); pSrc += 16;
in2 = vld1q_u8(pSrc); pSrc += 16;
in3 = vld1q_u8(pSrc); pSrc += 16;
in0 &= mask;
in1 &= mask;
in2 &= mask;
in3 &= mask;
in0 = vpaddq_u8(in0, in1);
in2 = vpaddq_u8(in2, in3);
in0 = vpaddq_u8(in0, in2);
out = vpadd_u8(vget_low_u8(in0), vget_high_u8(in0));
vst1_u8(pDst, out); pDst += 8;
length -= 64;
} while (length >=0);
pSrc += length>>3;
pDst += length;
} while (length > -64);
}
然而,Neon 具有非常用户友好且高效的排列和位操作指令,允许“垂直”
void foo_advanced(uint8_t *pDst, uint8_t *pSrc, intptr_t length)
{
//assert(length >= 128);
//assert(length & 7 == 0);
uint8x16x4_t in0, in1;
uint8x16x2_t row04, row15, row26, row37;
length -= 128;
do {
do {
in0 = vld4q_u8(pSrc); pSrc += 64;
in1 = vld4q_u8(pSrc); pSrc += 64;
row04 = vuzpq_u8(in0.val[0], in1.val[0]);
row15 = vuzpq_u8(in0.val[1], in1.val[1]);
row26 = vuzpq_u8(in0.val[2], in1.val[2]);
row37 = vuzpq_u8(in0.val[3], in1.val[3]);
row04.val[0] = vsliq_n_u8(row04.val[0], row15.val[0], 1);
row26.val[0] = vsliq_n_u8(row26.val[0], row37.val[0], 1);
row04.val[1] = vsliq_n_u8(row04.val[1], row15.val[1], 1);
row26.val[1] = vsliq_n_u8(row26.val[1], row37.val[1], 1);
row04.val[0] = vsliq_n_u8(row04.val[0], row26.val[0], 2);
row04.val[1] = vsliq_n_u8(row04.val[1], row26.val[1], 2);
row04.val[0] = vsliq_n_u8(row04.val[0], row04.val[1], 4);
vst1q_u8(pDst, row04.val[0]); pDst += 16;
length -= 128;
} while (length >=0);
pSrc += length>>3;
pDst += length;
} while (length > -128);
}
仅限 Neon 的高级版本更短更快,但 GCC
在处理 Neon 特定排列指令(例如 vtrn
、vzip
和 [=16] 时非常糟糕=].
https://godbolt.org/z/bGdbohqKe
Clang
也好不到哪里去:它会发送不必要的垃圾邮件 vorr
,其中 GCC
与 vmov
相同。
.syntax unified
.arm
.arch armv7-a
.fpu neon
.global foo_asm
.text
.func
.balign 64
foo_asm:
sub r2, r2, #128
.balign 16
1:
vld4.8 {d16, d18, d20, d22}, [r1]!
vld4.8 {d17, d19, d21, d23}, [r1]!
vld4.8 {d24, d26, d28, d30}, [r1]!
vld4.8 {d25, d27, d29, d31}, [r1]!
subs r2, r2, #128
vuzp.8 q8, q12
vuzp.8 q9, q13
vuzp.8 q10, q14
vuzp.8 q11, q15
vsli.8 q8, q9, #1
vsli.8 q10, q11, #1
vsli.8 q12, q13, #1
vsli.8 q14, q15, #1
vsli.8 q8, q10, #2
vsli.8 q12, q14, #2
vsli.8 q8, q12, #4
vst1.8 {q8}, [r0]!
bpl 1b
add r1, r1, r2
cmp r2, #-128
add r0, r0, r2, asr #3
bgt 1b
.balign 8
bx lr
.endfunc
.end
最内层循环包括:
GCC:32 条指令
叮当声:30 条指令
汇编:18 条指令
不需要火箭科学就可以找出最快的速度和速度:如果您要进行排列,请不要相信编译器。
站在Jake 'Alquimista' LEE
的肩膀上,我们可以通过改变zip和vlsi运算符的顺序来改进解压指令和算法:
#define interleave_nibbles(top) \
top.val[0] = vsliq_n_u8(top.val[0], top.val[1],1);\
top.val[2] = vsliq_n_u8(top.val[2], top.val[3],1);\
top.val[0] = vsliq_n_u8(top.val[0], top.val[2],2);
void transpose_bits(uint8_t const *src, uint8_t *dst) {
uint8x16x4_t top = vld4q_u8(src);
uint8x16x4_t bot = vld4q_u8(src + 64); src+=128;
interleave_nibbles(top);
interleave_nibbles(bot);
// now we have 4 bits correct in each of the 32 bytes left
// top = 0to3 4to7 8to11 12to15 ...
// bot = 64to67 68to71 ...
uint8x16x2_t top_bot = vuzpq_u8(top.val[0], bot.val[0]);
uint8x16_t result = vsliq_n_u8(top_bot.val[0], top_bot.val[1], 4);
vst1q_u8(dst, result); dst += 16;
}
clang 生成的汇编程序现在只有两个无关的 movs(通过 or),gcc 输出有四个 movs。
vld4.8 {d16, d18, d20, d22}, [r0]!
vld4.8 {d17, d19, d21, d23}, [r0]!
vld4.8 {d24, d26, d28, d30}, [r0]!
vsli.8 q10, q11, #1
vorr q0, q8, q8
vld4.8 {d25, d27, d29, d31}, [r0]
vsli.8 q0, q9, #1
vorr q2, q14, q14
vsli.8 q12, q13, #1
vsli.8 q2, q15, #1
vsli.8 q0, q10, #2
vsli.8 q12, q2, #2
vuzp.8 q0, q12
vsli.8 q0, q12, #4
vst1.8 {d0, d1}, [r1]
而且 arm64 版本看起来很完美,只有 12 条指令。
ld4 { v0.16b, v1.16b, v2.16b, v3.16b }, [x0], #64
ld4 { v4.16b, v5.16b, v6.16b, v7.16b }, [x0]
sli v0.16b, v1.16b, #1
sli v2.16b, v3.16b, #1
sli v0.16b, v2.16b, #2
sli v4.16b, v5.16b, #1
sli v6.16b, v7.16b, #1
sli v4.16b, v6.16b, #2
uzp1 v16.16b, v0.16b, v4.16b
uzp2 v0.16b, v0.16b, v4.16b
sli v16.16b, v0.16b, #4
str q16, [x1]
我正在执行一项任务,将每个像素有 8 位 (uint8_t
) 且每个像素只能为 0 或 1(或 255)的大型二进制标签图像转换为一个数组uint64_t
个数字,uint64_t
个数字中的每一位代表一个标签像素。
例如,
输入数组:0 1 1 0 ... (00000000 00000001 00000001 00000000 ...)
或输入数组:0 255 255 0 ... (00000000 11111111 11111111 00000000 ...)
输出数组(number):6
(因为每个uint8_t
转换成bit后变成0110
)
目前实现这个的C代码是:
for (int j = 0; j < width >> 6; j++) {
uint8_t* in_ptr= in + (j << 6);
uint64_t out_bits = 0;
if (in_ptr[0]) out_bits |= 0x0000000000000001;
if (in_ptr[1]) out_bits |= 0x0000000000000002;
.
.
.
if (in_ptr[63]) out_bits |= 0x8000000000000000;
*output = obits; output ++;
}
ARM NEON 能否优化此功能?请帮忙。谢谢!
假设输入值为 0 或 255,下面是基本版本,非常简单,特别适合有英特尔 SSE/AVX 经验的人。
void foo_basic(uint8_t *pDst, uint8_t *pSrc, intptr_t length)
{
//assert(length >= 64);
//assert(length & 7 == 0);
uint8x16_t in0, in1, in2, in3;
uint8x8_t out;
const uint8x16_t mask = {1, 2, 4, 8, 16, 32, 64, 128, 1, 2, 4, 8, 16, 32, 64, 128};
length -= 64;
do {
do {
in0 = vld1q_u8(pSrc); pSrc += 16;
in1 = vld1q_u8(pSrc); pSrc += 16;
in2 = vld1q_u8(pSrc); pSrc += 16;
in3 = vld1q_u8(pSrc); pSrc += 16;
in0 &= mask;
in1 &= mask;
in2 &= mask;
in3 &= mask;
in0 = vpaddq_u8(in0, in1);
in2 = vpaddq_u8(in2, in3);
in0 = vpaddq_u8(in0, in2);
out = vpadd_u8(vget_low_u8(in0), vget_high_u8(in0));
vst1_u8(pDst, out); pDst += 8;
length -= 64;
} while (length >=0);
pSrc += length>>3;
pDst += length;
} while (length > -64);
}
然而,Neon 具有非常用户友好且高效的排列和位操作指令,允许“垂直”
void foo_advanced(uint8_t *pDst, uint8_t *pSrc, intptr_t length)
{
//assert(length >= 128);
//assert(length & 7 == 0);
uint8x16x4_t in0, in1;
uint8x16x2_t row04, row15, row26, row37;
length -= 128;
do {
do {
in0 = vld4q_u8(pSrc); pSrc += 64;
in1 = vld4q_u8(pSrc); pSrc += 64;
row04 = vuzpq_u8(in0.val[0], in1.val[0]);
row15 = vuzpq_u8(in0.val[1], in1.val[1]);
row26 = vuzpq_u8(in0.val[2], in1.val[2]);
row37 = vuzpq_u8(in0.val[3], in1.val[3]);
row04.val[0] = vsliq_n_u8(row04.val[0], row15.val[0], 1);
row26.val[0] = vsliq_n_u8(row26.val[0], row37.val[0], 1);
row04.val[1] = vsliq_n_u8(row04.val[1], row15.val[1], 1);
row26.val[1] = vsliq_n_u8(row26.val[1], row37.val[1], 1);
row04.val[0] = vsliq_n_u8(row04.val[0], row26.val[0], 2);
row04.val[1] = vsliq_n_u8(row04.val[1], row26.val[1], 2);
row04.val[0] = vsliq_n_u8(row04.val[0], row04.val[1], 4);
vst1q_u8(pDst, row04.val[0]); pDst += 16;
length -= 128;
} while (length >=0);
pSrc += length>>3;
pDst += length;
} while (length > -128);
}
仅限 Neon 的高级版本更短更快,但 GCC
在处理 Neon 特定排列指令(例如 vtrn
、vzip
和 [=16] 时非常糟糕=].
https://godbolt.org/z/bGdbohqKe
Clang
也好不到哪里去:它会发送不必要的垃圾邮件 vorr
,其中 GCC
与 vmov
相同。
.syntax unified
.arm
.arch armv7-a
.fpu neon
.global foo_asm
.text
.func
.balign 64
foo_asm:
sub r2, r2, #128
.balign 16
1:
vld4.8 {d16, d18, d20, d22}, [r1]!
vld4.8 {d17, d19, d21, d23}, [r1]!
vld4.8 {d24, d26, d28, d30}, [r1]!
vld4.8 {d25, d27, d29, d31}, [r1]!
subs r2, r2, #128
vuzp.8 q8, q12
vuzp.8 q9, q13
vuzp.8 q10, q14
vuzp.8 q11, q15
vsli.8 q8, q9, #1
vsli.8 q10, q11, #1
vsli.8 q12, q13, #1
vsli.8 q14, q15, #1
vsli.8 q8, q10, #2
vsli.8 q12, q14, #2
vsli.8 q8, q12, #4
vst1.8 {q8}, [r0]!
bpl 1b
add r1, r1, r2
cmp r2, #-128
add r0, r0, r2, asr #3
bgt 1b
.balign 8
bx lr
.endfunc
.end
最内层循环包括:
GCC:32 条指令
叮当声:30 条指令
汇编:18 条指令
不需要火箭科学就可以找出最快的速度和速度:如果您要进行排列,请不要相信编译器。
站在Jake 'Alquimista' LEE
的肩膀上,我们可以通过改变zip和vlsi运算符的顺序来改进解压指令和算法:
#define interleave_nibbles(top) \
top.val[0] = vsliq_n_u8(top.val[0], top.val[1],1);\
top.val[2] = vsliq_n_u8(top.val[2], top.val[3],1);\
top.val[0] = vsliq_n_u8(top.val[0], top.val[2],2);
void transpose_bits(uint8_t const *src, uint8_t *dst) {
uint8x16x4_t top = vld4q_u8(src);
uint8x16x4_t bot = vld4q_u8(src + 64); src+=128;
interleave_nibbles(top);
interleave_nibbles(bot);
// now we have 4 bits correct in each of the 32 bytes left
// top = 0to3 4to7 8to11 12to15 ...
// bot = 64to67 68to71 ...
uint8x16x2_t top_bot = vuzpq_u8(top.val[0], bot.val[0]);
uint8x16_t result = vsliq_n_u8(top_bot.val[0], top_bot.val[1], 4);
vst1q_u8(dst, result); dst += 16;
}
clang 生成的汇编程序现在只有两个无关的 movs(通过 or),gcc 输出有四个 movs。
vld4.8 {d16, d18, d20, d22}, [r0]!
vld4.8 {d17, d19, d21, d23}, [r0]!
vld4.8 {d24, d26, d28, d30}, [r0]!
vsli.8 q10, q11, #1
vorr q0, q8, q8
vld4.8 {d25, d27, d29, d31}, [r0]
vsli.8 q0, q9, #1
vorr q2, q14, q14
vsli.8 q12, q13, #1
vsli.8 q2, q15, #1
vsli.8 q0, q10, #2
vsli.8 q12, q2, #2
vuzp.8 q0, q12
vsli.8 q0, q12, #4
vst1.8 {d0, d1}, [r1]
而且 arm64 版本看起来很完美,只有 12 条指令。
ld4 { v0.16b, v1.16b, v2.16b, v3.16b }, [x0], #64
ld4 { v4.16b, v5.16b, v6.16b, v7.16b }, [x0]
sli v0.16b, v1.16b, #1
sli v2.16b, v3.16b, #1
sli v0.16b, v2.16b, #2
sli v4.16b, v5.16b, #1
sli v6.16b, v7.16b, #1
sli v4.16b, v6.16b, #2
uzp1 v16.16b, v0.16b, v4.16b
uzp2 v0.16b, v0.16b, v4.16b
sli v16.16b, v0.16b, #4
str q16, [x1]