ARM NEON 优化不比 C++ 指针实现快

ARM NEON Optimization no faster than C++ Pointer Implementation

我有 2 个函数可以将 YUYV 帧拆分为 Y/U/V 个独立的平面。我这样做是为了通过将包含 Y/U/V 数据的 3 个纹理上传到 GPU,在 OpenGL ES 2.0 着色器中执行从 YUYV 视频帧到 RGBA 的格式转换。其中一个函数是用 C++ 编写的,一个是用 ARM NEON 编写的。我的目标是 Cortex-A15 AM57xx Sitara。

我希望 NEON 代码的性能优于 C++ 代码,但它们的性能相同。一种可能是我受内存 I/O 限制。另一种可能是我不擅长写 NEON 代码..

为什么这两个函数执行相同的操作?是否可以对这两个功能进行明显的优化?

霓虹灯功能:

/// This structure is passed to ARM Assembly code
/// to split the YUV frame into seperate planes for
/// OpenGL Consumption
typedef struct {
    char *input_data;
    int input_size;
    char *y_plane;
    char *u_plane;
    char *v_plane;
} yuvSplitStruct;

void TopOpenGL::splitYuvPlanes(yuvSplitStruct *yuvStruct)
{

    __asm__ volatile(

                "PUSH {r4}\n"                            /* Save callee-save registers R4 and R5 on the stack */
                "PUSH {r5}\n"                            /* r1 is the pointer to the input structure ( r0 is 'this' because c++ ) */
                "ldr r0 , [r1]\n"                        /* reuse r0 scratch register for the address of our frame input */
                "ldr r2 , [r1, #4]\n"                    /* use r2 scratch register to store the size in bytes of the YUYV frame */
                "ldr r3 , [r1, #8]\n"                    /* use r3 scratch register to store the destination Y plane address */
                "ldr r4 , [r1, #12]\n"                   /* use r4 register to store the destination U plane address */
                "ldr r5 , [r1, #16]\n"                   /* use r5 register to store the destination V plane address */
                "/* pld [r0, #192] PLD Does not seem to help */"
                    "mov r2, r2, lsr #5\n"               /* Divide number of bytes by 32 because we process 16 pixels at a time */
                    "loopYUYV:\n"
                        "vld4.8 {d0-d3}, [r0]!\n"        /* Load 8 YUYV elements from our frame into d0-d3, increment frame pointer */
                        "vst2.8 {d0,d2}, [r3]!\n"        /* Store both Y elements into destination y plane, increment plane pointer */
                        "vmov.F64 d0, d1\n"              /* Duplicate U value */
                        "vst2.8 {d0,d1}, [r4]!\n"        /* Store both U elements into destination u plane, increment plane pointer */
                        "vmov.F64 d1, d3\n"              /* Duplicate V value */
                        "vst2.8 {d1,d3}, [r5]!\n"        /* Store both V elements into destination v plane, increment plane pointer */
                        "subs r2, r2, #1\n"              /* Decrement the loop counter */
                    "bgt loopYUYV\n"                     /* Loop until entire frame is processed */
                "POP {r5}\n"                             /* Restore callee-save registers */
                "POP {r4}\n"
    );

}

C++ 函数:

void TopOpenGL::splitYuvPlanes(unsigned char *data, int size, unsigned char *y, unsigned char *u, unsigned char *v)
{

    for ( int c = 0 ; c < ( size - 4 ) ; c+=4 ) {

        *y = *data; // Y0
        data++;
        *u = *data; // U0
        u++;
        *u = *data; // U0
        data++;
        y++;
        *y = *data; // Y1
        data++;
        *v = *data; // V0
        v++;
        *v = *data; // V0

        data++;
        y++;
        u++;
        v++;
    }

}

这个问题可能涉及两个不同的因素: 1. Neon 指令是否比 "regular" ARM 指令快? 2.我能写出比编译器更好的汇编吗?

Neon 指令是否比 "regular" ARM 指令快?

您的算法只涉及加载数据并将其存储在别处。在 A15 上,架构中的 Load/Store 流水线为 NEON 寄存器和 ARM 寄存器共享。这可能不是全貌,但 A8 和 A9 过去可能存在的任何好处,它们具有不同的 load/store 流水线以及不同的指令发布逻辑、不同的指令重新排序和分支预测功能。因此,在 A15 上,在考虑 NEON 指令与常规 ARM 指令时,这些考虑因素不再是一个重要因素。即使在那时,memcpy 在 ARM 指令中比 NEON 更快。

现在很老的 A8 的一个很好的介绍是 http://infocenter.arm.com/help/index.jsp?topic=/com.arm.doc.faqs/ka13544.html

A15 上的超标量架构视图:http://www.extremetech.com/wp-content/uploads/2012/11/Cortex-A15Block.jpg

您可以将其与 A8 进行比较:

http://courses.cecs.anu.edu.au/courses/ENGN8537/notes/images/processor/arm-a8-pipeline.png

请注意,在 A8 上,NEON 是一个非常独立的块,但在 A15 上,很多东西是共享的。

我能写出比编译器更好的汇编吗?

也许吧,但是对于现代架构,这现在涉及对微架构的越来越深入的理解,尤其是对于仅是数据的操作 permuting/interleaving。如果您正在编写实际上涉及乘法的更复杂的数据处理,那么是的,通常您可以比编译器做得更好,特别是将循环展开调整为乘法的回写延迟。展开循环需要努力说服编译器去做,因为这通常会限制数据的长度(例如,它是 4 的倍数吗?)。使用 load/stores 时,不需要进行有趣的优化,因为数学运算没有回写延迟。

关于流水线处理器架构的内容很多,但是

https://en.wikipedia.org/wiki/Classic_RISC_pipeline#Writeback