通过ARM NEON汇编实现元素级乘法的最大优化

8

我正在针对双核Cortex-A9处理器优化两个一维数组的逐元素乘法。开发板上运行Linux,并且我正在使用GCC 4.5.2编译器。

以下是我的C++内联汇编函数。src1、src2和dst都是16字节对齐的。

更新:可测试代码:

void Multiply(
    const float* __restrict__ src1,
    const float* __restrict__ src2,
    float* __restrict__ dst,
    const unsigned int width,
    const unsigned int height)
{
    int loopBound = (width * height) / 4;
    asm volatile(
        ".loop:                             \n\t"
        "vld1.32  {q1}, [%[src1]:128]!      \n\t"
        "vld1.32  {q2}, [%[src2]:128]!      \n\t"
        "vmul.f32 q0, q1, q2                \n\t"
        "vst1.32  {q0}, [%[dst]:128]!       \n\t"
        "subs     %[lBound], %[lBound], $1  \n\t"
        "bge      .loop                     \n\t"
        :
        :[dst] "r" (dst), [src1] "r" (src1), [src2] "r" (src2),
        [lBound] "r" (loopBound)
        :"memory", "d0", "d1", "d2", "d3", "d4", "d5
    );
}

//The following function describes how to test the element wise multiplication
void Test()
{
    const unsigned int width = 1024, height = 1024;
    float* src1 __attribute__((aligned(16))) = new float[width * height];
    float* src2 __attribute__((aligned(16))) = new float[width * height];
    float* dst __attribute__((aligned(16))) = new float[width * height];
    for(unsigned int i = 0; i < (width * height); i++)
    {
        src1[i] = (float)rand();
        src2[i] = (float)rand();
    }
    Multiply(src1, src2, dst, width, height);

    std::cout << dst[0] << std::endl;
}

计算1024*1024个数值需要约0.016秒的时间。(两个线程:每个线程计算数组的一半)粗略解释,一个迭代的计算需要122个周期。这似乎有点慢。但瓶颈在哪里呢?
我甚至尝试了使用“pld”命令预加载L2缓存中的元素,“展开”循环以每次计算多达20个值,并重新排列指令以确保处理器不等待内存。但我没有得到太大的加速(最多快了0.001秒)。
你有什么建议可以加速计算吗?

你只用单线程对代码进行了分析吗? - Aki Suihkonen
你是否检查过指针的低位,以验证属性(aligned(16))是否产生了预期的代码? - Aki Suihkonen
2
新的float[width * height]; -此处运算符“new”不提供对齐内存。尝试为测试使用静态数组,或者动态分配内存,但必须手动对齐结果指针。 - Smalti
2
你还应该通过使用向量指令来移动数据,以查看可以获得的最高值,对内存进行基准测试。 - auselen
1
是的 - 通过 'new' 进行动态分配,其中包括 'malloc' 和 C++ 构造函数调用,但您可以使用 'memalign' 函数进行动态对齐分配,例如: size_t alignment = 16; void* ptr = memalign( alignment, mem_size ); - 这应该足够了。之后,您可以在命令中使用 :128 对齐后缀。 但这并不能解决您的问题,因为通常它只能提高约 ~5% 的性能(在我的测试中)。 - Smalti
显示剩余18条评论
1个回答

1

我对NEON并不是很了解。然而,我认为您可能存在数据依赖性导致性能问题。我建议您在循环开始时进行一些加载,然后将它们放置在乘法存储之间。我认为存储可能会阻塞,直到乘法完成。

    asm volatile(
    "vld1.32  {q1}, [%[src1]:128]!      \n\t"
    "vld1.32  {q2}, [%[src2]:128]!      \n\t"
    ".loop:                             \n\t"
    "vmul.f32 q0, q1, q2                \n\t"
    "vld1.32  {q1}, [%[src1]:128]!      \n\t"
    "vld1.32  {q2}, [%[src2]:128]!      \n\t"
    "vst1.32  {q0}, [%[dst]:128]!       \n\t"
    "subs     %[lBound], %[lBound], $1  \n\t"
    "bge      .loop                     \n\t"
    :
    :[dst] "r" (dst), [src1] "r" (src1), [src2] "r" (src2),
    [lBound] "r" (loopBound)
    :"memory", "d0", "d1", "d2", "d3", "d4", "d5
);

这样你就可以使用乘法并行处理负载。你需要过度分配源数组或更改循环索引并进行最后的乘法和存储。如果NEON操作不影响条件码,也可以重新排序subs并将其放在前面。
编辑:实际上,Cortex A-9媒体处理引擎文档建议交错ARM和NEON指令,因为它们可以并行执行。此外,NEON指令似乎会设置FPSCR而不是ARM CPSR,因此重新排序subs会减少执行时间。您还可以对循环进行缓存对齐。

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