使用NEON优化RGBA8888到RGB565的转换

我试图使用NEON向量指令集来优化iOS上的图像格式转换。 我认为这将很好地映射,因为它处理了一堆类似的数据。

然而,我的尝试并没有那么好,只是实现了一个边缘的加速比天真的C实现:

for(int i = 0; i < pixelCount; ++i, ++inPixel32) { const unsigned int r = ((*inPixel32 >> 0 ) & 0xFF); const unsigned int g = ((*inPixel32 >> 8 ) & 0xFF); const unsigned int b = ((*inPixel32 >> 16) & 0xFF); *outPixel16++ = ((r >> 3) << 11) | ((g >> 2) << 5) | ((b >> 3) << 0); } 

iPad 2上的1百万像素图像arrays:

格式是[min avg max n =定时器采样数],以毫秒为单位

C:[14.446 14.632 18.405 n = 1000] ms

NEON:[11.920 12.032 15.336 n = 1000] ms

我在NEON实施的尝试如下:

  int i; const int pixelsPerLoop = 8; for(i = 0; i < pixelCount; i += pixelsPerLoop, inPixel32 += pixelsPerLoop, outPixel16 += pixelsPerLoop) { //Read all r,g,b pixels into 3 registers uint8x8x4_t rgba = vld4_u8(inPixel32); //Right-shift r,g,b as appropriate uint8x8_t r = vshr_n_u8(rgba.val[0], 3); uint8x8_t g = vshr_n_u8(rgba.val[1], 2); uint8x8_t b = vshr_n_u8(rgba.val[2], 3); //Widen b uint16x8_t r5_g6_b5 = vmovl_u8(b); //Widen r uint16x8_t r16 = vmovl_u8(r); //Left shift into position within 16-bit int r16 = vshlq_n_u16(r16, 11); r5_g6_b5 |= r16; //Widen g uint16x8_t g16 = vmovl_u8(g); //Left shift into position within 16-bit int g16 = vshlq_n_u16(g16, 5); r5_g6_b5 |= g16; //Now write back to memory vst1q_u16(outPixel16, r5_g6_b5); } //Do the remainder on normal flt hardware 

代码是通过LLVM 3.0编译成以下(删除了.loc和额外的标签):

 _DNConvert_ARGB8888toRGB565: push {r4, r5, r7, lr} mov r9, r1 mov.w r12, #0 add r7, sp, #8 cmp r2, #0 mov.w r1, #0 it ne movne r1, #1 cmp r0, #0 mov.w r3, #0 it ne movne r3, #1 cmp.w r9, #0 mov.w r4, #0 it ne movne r4, #1 tst.w r9, #3 bne LBB0_8 ands r1, r3 ands r1, r4 cmp r1, #1 bne LBB0_8 movs r1, #0 lsr.w lr, r9, #2 cmp.w r1, r9, lsr #2 bne LBB0_9 mov r3, r2 mov r5, r0 b LBB0_5 LBB0_4: movw r1, #65528 add.w r0, lr, #7 movt r1, #32767 ands r1, r0 LBB0_5: mov.w r12, #1 cmp r1, lr bhs LBB0_8 rsb r0, r1, r9, lsr #2 mov.w r9, #63488 mov.w lr, #2016 mov.w r12, #1 LBB0_7: ldr r2, [r5], #4 subs r0, #1 and.w r1, r9, r2, lsl #8 and.w r4, lr, r2, lsr #5 ubfx r2, r2, #19, #5 orr.w r2, r2, r4 orr.w r1, r1, r2 strh r1, [r3], #2 bne LBB0_7 LBB0_8: mov r0, r12 pop {r4, r5, r7, pc} LBB0_9: sub.w r1, lr, #1 movs r3, #32 add.w r3, r3, r1, lsl #2 bic r3, r3, #31 adds r5, r0, r3 movs r3, #16 add.w r1, r3, r1, lsl #1 bic r1, r1, #15 adds r3, r2, r1 movs r1, #0 LBB0_10: vld4.8 {d16, d17, d18, d19}, [r0]! adds r1, #8 cmp r1, lr vshr.u8 d20, d16, #3 vshr.u8 d21, d17, #2 vshr.u8 d16, d18, #3 vmovl.u8 q11, d20 vmovl.u8 q9, d21 vmovl.u8 q8, d16 vshl.i16 q10, q11, #11 vshl.i16 q9, q9, #5 vorr q8, q8, q10 vorr q8, q8, q9 vst1.16 {d16, d17}, [r2]! Ltmp28: blo LBB0_10 b LBB0_4 

完整的代码可在https://github.com/darknoon/DNImageConvert我将不胜感激任何帮助,谢谢!

在这里,为XCode准备了手动优化的NEON实现:

 /* IT DOESN'T WORK!!! USE THE NEXT VERSION BELOW. * BGRA2RGB565.s * * Created by Jake "Alquimista" Lee on 11. 11. 1.. * Copyright 2011 Jake Lee. All rights reserved. */ .align 2 .globl _bgra2rgb565_neon .private_extern _bgra2rgb565_neon // unsigned int * bgra2rgb565_neon(unsigned int * pDst, unsigned int * pSrc, unsigned int count); //ARM pDst .req r0 pSrc .req r1 count .req r2 //NEON blu .req d16 grn .req d17 red .req d18 alp .req d19 rg .req red gb .req blu _bgra2rgb565_neon: pld [pSrc] tst count, #0x7 movne r0, #0 bxne lr loop: pld [pSrc, #32] vld4.8 {blu, grn, red, alp}, [pSrc]! subs count, count, #8 vshr.u8 red, red, #3 vext.8 rg, grn, red, #5 vshr.u8 grn, grn, #2 vext.8 gb, blu, grn, #3 vst2.8 {gb, rg}, [pDst]! bgt loop bx lr 

这个版本会比你build议的要快很多倍:

  • 通过PLD提高caching命中率

  • 转换为“长”没有必要

  • 循环内的指令更less

尽pipe如此,还是有一些优化的余地,你可以修改循环,使其每个迭代转换16个像素,而不是8个。然后,你可以安排指令,以完全避免这两个摊位(这是不可能在这个8 /迭代版本以上),并受益于NEON的双重能力。

我没有这样做,因为它会使代码难以理解。

了解VEXT应该做什么很重要。

现在取决于你。 🙂

我validation了这个代码在Xcode下正确编译。 虽然我很确定它也能正常工作,但我不能保证这一点,因为我没有testing环境。 如果发生故障,请告诉我。 那么我会相应地纠正它。

CYA

================================================== ============================

那么,这是改进版本。

由于VSRI指令的性质不允许目标以外的两个操作数,因此无法针对寄存器分配创build更强大的操作数。

请检查您的源图像的图像格式。 (确切的元素字节顺序)

如果不是B,G,R,A(它是iOS上的默认和本机版本),那么您的应用程序将受到iOS内部转换的严重影响。

如果因为任何原因绝对不可能改变这个,请告诉我。 我会写一个匹配它的新版本。

PS:我忘了在函数原型开始时删除下划线。 现在不见了

 /* * BGRA2RGB565.s * * Created by Jake "Alquimista" Lee on 11. 11. 1.. * Copyright 2011 Jake Lee. All rights reserved. * * Version 1.1 * - bug fix * * Version 1.0 * - initial release */ .align 2 .globl _bgra2rgb565_neon .private_extern _bgra2rgb565_neon // unsigned int * bgra2rgb565_neon(unsigned int * pDst, unsigned int * pSrc, unsigned int count); //ARM pDst .req r0 pSrc .req r1 count .req r2 //NEON blu .req d16 grn .req d17 red .req d18 alp .req d19 gb .req grn rg .req red _bgra2rgb565_neon: pld [pSrc] tst count, #0x7 movne r0, #0 bxne lr .loop: pld [pSrc, #32] vld4.8 {blu, grn, red, alp}, [pSrc]! subs count, count, #8 vsri.8 red, grn, #5 vshl.u8 gb, grn, #3 vsri.8 gb, blu, #3 vst2.8 {gb, rg}, [pDst]! bgt .loop bx lr 

如果您在iOS或OS X上,那么您可能很高兴在Accelerate.framework中发现vImageConvert_RGBA8888toRGB565()和朋友。 该函数将8位值舍入为最接近的565值。

为了更好的抖动,其质量几乎与8位颜色无法区分,请尝试vImageConvert_AnyToAny():

 vImage_CGImageFormat RGBA8888Format = { .bitsPerComponent = 8, .bitsPerPixel = 32, .bitmapInfo = kCGBitmapByteOrderDefault | kCGImageAlphaNoneSkipLast, .colorSpace = NULL, // sRGB or substitute your own in }; vImage_CGImageFormat RGB565Format = { .bitsPerComponent = 5, .bitsPerPixel = 16, .bitmapInfo = kCGBitmapByteOrder16Little | kCGImageAlphaNone, .colorSpace = RGBA8888Format.colorSpace, }; err = vImageConverterRef converter = vImageConverter_CreateWithCGImageFormat( &RGBA8888Format, &RGB565Format, NULL, kvImageNoFlags, &err ); err = vImageConvert_AnyToAny( converter, &src, &dest, NULL, kvImageNoFlags ); 

为了获得最佳性能,这些方法都是vector化和multithreading化的。

您可能需要使用vld4q_u8()而不是vld4_u8(),并相应地调整其余代码。 很难说出问题的可能性,但是汇编程序看起来不会太糟糕。

(我对NEON并不熟悉,也不熟悉Ipad2的内存系统,但这是我们以前用88110像素操作所做的,这是当今SIMD扩展的早期前兆)

内存延迟有多大?

你可以通过展开内部循环并在ARM上从存储器中取出“下一个”值的时候运行NEON指令来隐藏它吗? 对NEON手册的简要介绍意味着您可以并行运行ARM和NEON指令。

我不认为将vld4_u8转换为vld4q_u8会导致性能的提高。

代码看起来很简单。 我不擅长ASM,因此需要一些时间深入研究。

霓虹似乎很简单。 但我并不清楚r5_g6_b5 | = g16被用来代替vorrq_u16

请查看优化级别。 据我所知,霓虹灯代码优化级别达到最大值1.因此,当参考代码和霓虹灯代码都将默认优化考虑在内时,性能可能会有所不同,因为DEFAULT引用的优化级别可能是不同。

我没有find任何可以更好地使用当前代码的区域。