使用NEON汇编进行优化

5

我正在尝试使用NEON优化OpenCV代码的某些部分。这是我要处理的原始代码块。(注意:如果有必要,您可以在“opencvfolder/modules/video/src/lkpyramid.cpp”找到完整的源代码。它是一个对象跟踪算法的实现。)

for( ; x < colsn; x++ )
{
    deriv_type t0 = (deriv_type)(trow0[x+cn] - trow0[x-cn]);
    deriv_type t1 = (deriv_type)((trow1[x+cn] + trow1[x-cn])*3 + trow1[x]*10);
    drow[x*2] = t0; drow[x*2+1] = t1;

}

在这段代码中,deriv_type 的大小为 2 个字节。 以下是我编写的 NEON 汇编代码。使用原始代码时我测得 10-11 帧每秒,但使用了 NEON 后效果更差,只有 5-6 帧每秒。我对 NEON 并不是很了解,可能这段代码存在许多错误。请问我哪里出错了呢?谢谢。

for( ; x < colsn; x+=4 )
{
    __asm__ __volatile__(
    "vld1.16 d2, [%2] \n\t" // d2 = trow0[x+cn]
    "vld1.16 d3, [%3] \n\t" // d3 = trow0[x-cn]
    "vsub.i16 d9, d2, d3 \n\t" // d9 = d2 - d3

    "vld1.16 d4, [%4] \n\t" // d4 = trow1[x+cn]
    "vld1.16 d5, [%5] \n\t" // d5 = trow1[x-cn]
    "vld1.16 d6, [%6] \n\t" // d6 = trow1[x]

    "vmov.i16 d7, #3 \n\t"  // d7 = 3
    "vmov.i16 d8, #10 \n\t" // d8 = 10


    "vadd.i16 d4, d4, d5 \n\t" // d4 = d4 + d5
    "vmul.i16 d10, d4, d7 \n\t" // d10 = d4 * d7
    "vmla.i16 d10, d6, d8 \n\t" // d10 = d10 + d6 * d8

    "vst2.16 {d9,d10}, [%0] \n\t" // drow[x*2] = d9; drow[x*2+1] = d10;
    //"vst1.16 d4, [%1] \n\t"

    :   //output
    :"r"(drow+x*2), "r"(drow+x*2+1), "r"(trow0+x+cn), "r"(trow0+x-cn), "r"(trow1+x+cn), "r"(trow1+x-cn), "r"(trow1) //input
    :"d2", "d3", "d4", "d5", "d6", "d7", "d8", "d9", "d10"  //registers


    );
}

编辑

这是使用内置功能的版本。它几乎与之前的版本相同。它仍然运行缓慢。

const int16x8_t vk3 = { 3, 3, 3, 3, 3, 3, 3, 3 };
const int16x8_t vk10 = { 10, 10, 10, 10, 10, 10, 10, 10 };

for( ; x < colsn; x+=8 )
{
                int16x8x2_t loaded;
                int16x8_t t0a = vld1q_s16(&trow0[x + cn]);
                int16x8_t t0b = vld1q_s16(&trow0[x - cn]);
                loaded.val[0] = vsubq_s16(t0a, t0b); // t0 = (trow0[x + cn] - trow0[x - cn])

                loaded.val[1] = vld1q_s16(&trow1[x + cn]);
                int16x8_t t1b = vld1q_s16(&trow1[x - cn]);
                int16x8_t t1c = vld1q_s16(&trow1[x]);

                loaded.val[1] = vaddq_s16(loaded.val[1], t1b);
                loaded.val[1] = vmulq_s16(loaded.val[1], vk3);
                loaded.val[1] = vmlaq_s16(loaded.val[1], t1c, vk10);
}

值得在详细的逐行统计分析器上运行此程序,以便发现问题。 - Sam
你需要在最佳距离处预加载缓存。这将提高你的性能;具体提升多少取决于你的CPU和内存。 - BitBank
2个回答

3
由于数据冲突,您正在创建许多管道停顿。例如,这三条指令:
"vadd.i16 d4, d4, d5 \n\t" // d4 = d4 + d5
"vmul.i16 d10, d4, d7 \n\t" // d10 = d4 * d7
"vmla.i16 d10, d6, d8 \n\t" // d10 = d10 + d6 * d8

他们每个指令只需要1个周期来发出,但是它们之间有几个周期的停顿,因为结果还没有准备好(NEON指令调度)。
尝试将循环展开几次并交错它们的指令。如果您使用内在函数,编译器可能会自动完成此操作。打败编译器在指令调度等方面并不是不可能,但非常困难,并且通常不值得这么做(这可能属于不要过早优化)。
编辑
您的内在代码是合理的,我怀疑编译器并没有做得很好。查看它生成的汇编代码(objdump -d),您可能会发现它也创建了许多流水线障碍。较新版本的编译器可能会有所帮助,但如果没有,您可能需要修改循环以隐藏结果的延迟(您将需要指令计时)。保留当前代码,因为它是正确的,并且应该可以被聪明的编译器进行优化。
您最终可能会得到类似以下内容的东西:
// do step 1 of first iteration
// ...
for (int i = 0; i < n - 1; i++) {
  // do step 1 of (i+1)th
  // do step 2 of (i)th
  // with their instructions interleaved
  // ...
}
// do step 2 of (n-1)th
// ...

你也可以将循环分成超过2个步骤,或者展开循环几次(例如,将i++改为i+=2,将循环体翻倍,在第二个部分将i改为i+1)。我希望这个答案能够帮到你,如果有什么不明白的地方,请让我知道!

1

那里有一些循环不变量的东西需要移动到for循环外面-这可能会有所帮助。

您还可以考虑使用全宽度SIMD操作,以便每个循环迭代可以处理8个点而不是4个。

最重要的是,您应该使用内部函数而不是原始汇编语言,以便编译器可以处理缝隙优化、寄存器分配、指令调度、循环展开等。

例如:

// constants - init outside loop

const int16x8_t vk3 = { 3, 3, 3, 3, 3, 3, 3, 3 };
const int16x8_t vk10 = { 10, 10, 10, 10, 10, 10, 10, 10 };

for( ; x < colsn; x += 8)
{
    int16x8_t t0a = vld1q_s16(&trow0[x + cn]);
    int16x8_t t0b = vld1q_s16(&trow0[x - cn]);
    int16x8_t t0 = vsubq_s16(t0a, t0b); // t0 = (trow0[x + cn] - trow0[x - cn])

    // ...
}

谢谢,我会尝试一下。但我读过gcc对内嵌函数的处理不是很好,所以我选择了汇编语言。你认为gcc能提供很多帮助吗? - akaya
我建议先使用固有函数让它正常工作 - 这肯定比原始的标量代码更快,而且可能已经足够好了。但如果不行,你可以总是通过汇编调整循环的某些部分 - 这样就可以兼顾两者的优点。 - Paul R
嗨,保罗,抱歉回复晚了。我已经使用内置函数使其工作,但是帧率仍然很低。可能还有其他我做错的事情。无论如何,还是谢谢你。 - akaya
你是否按建议切换到全宽 SIMD? - Paul R
我认为最好在结尾处摆脱中间的“loaded”变量,只需进行两个存储操作 - 很难确定编译器会如何处理它。另外一件事:你正在使用“gcc -O3”,是吗? - Paul R
你关于 loaded 是正确的。我又改了一下。之前没有考虑过 O3,但我查了一下确认 OpenCV 的 cmake 文件已经在使用它了。 - akaya

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接