【问题标题】:Using FMA (fused multiply) instructions for complex multiplication使用 FMA(融合乘法)指令进行复数乘法
【发布时间】:2023-03-24 12:24:01
【问题描述】:

我想利用可用的融合乘法加法/减法 CPU 指令来帮助在适当大小的数组上进行复杂的乘法运算。本质上,基本数学如下所示:

void ComplexMultiplyAddToArray(float* pDstR, float* pDstI, const float* pSrc1R, const float* pSrc1I, const float* pSrc2R, const float* pSrc2I, int len)
{
    for (int i = 0; i < len; ++i)
    {
        const float fSrc1R = pSrc1R[i];
        const float fSrc1I = pSrc1I[i];
        const float fSrc2R = pSrc2R[i];
        const float fSrc2I = pSrc2I[i];

        //  Perform complex multiplication on the input and accumulate with the output
        pDstR[i] += fSrc1R*fSrc2R - fSrc1I*fSrc2I;
        pDstI[i] += fSrc1R*fSrc2I + fSrc2R*fSrc1I;
    }
}

您可能会看到,数据是结构化的,其中我们有单独的实数和虚数数组。现在,假设我有以下函数可用作分别执行 ab+c 和 ab-c 的单个指令的内在函数:

float fmadd(float a, float b, float c);
float fmsub(float a, float b, float c);

天真地,我可以看到我可以用一个 fmadd 和一个 fmsub 替换 2 个乘法、一个加法和一个减法,如下所示:

//  Perform complex multiplication on the input and accumulate with the output
pDstR[i] += fmsub(fSrc1R, fSrc2R, fSrc1I*fSrc2I);
pDstI[i] += fmadd(fSrc1R, fSrc2I, fSrc2R*fSrc1I);

这导致了非常适度的性能改进,以及我认为的准确性,但我认为我真的错过了一些可以代数修改数学的东西,这样我就可以替换更多的 mult/add 或 mult/sub组合。在每一行中,都有一个额外的加法和一个额外的乘法,我觉得我可以转换为单个 fma,但令人沮丧的是,如果不更改操作顺序并得到错误的结果,我无法弄清楚如何做到这一点。任何有想法的数学专家?

就这个问题而言,目标平台可能并不那么重要,因为我知道这些指令存在于各种平台上。

【问题讨论】:

  • 实际上你并没有减少乘数,因为fSrc1I*fSrc2I 仍然存在。
  • 是的,但是前面的 mult/sub 已经被一个 fmsub 替换了,所以它确实加快了速度。
  • 在一次迭代中,您加载了 6 个floats,使用了 4 次乘法器,并使用了两次加法器,写入了 2 floats。通过将加法隐藏到乘法中,您可以节省加法器的时间,这在这种情况下不太可能成为瓶颈。我要尝试的第一件事是restrict 那些指针,以允许更积极地调度加载/存储指令。在那之后,内存带宽变得比计算更受关注。
  • 我忘了提到restrict 是一个C99 关键字。尽管大多数编译器在 C++ 模式下都支持自己的等效版本,但我仍然建议在 C99 模式下编译此函数并使用标准 restrict

标签: c++ floating-point fma


【解决方案1】:

我发现以下(有一点帮助)似乎可以得出正确的答案:

pDstR[i] = fmsub(fSrc1R, fSrc2R, fmsub(fSrc1I, fSrc2I, pDstR[i]));
pDstI[i] = fmadd(fSrc1R, fSrc2I, fmadd(fSrc2R, fSrc1I, pDstI[i]));

但奇怪的是,它在 AVX 上的性能提升不如使用半 FMA 保留数学的真实结果部分,而是使用完整 FMA 让假想结果:

pDstR[i] += fmsub(fSrc1R, fSrc2R, fSrc1I*fSrc2I);
pDstI[i] = fmadd(fSrc1R, fSrc2I, fmadd(fSrc2R, fSrc1I, pDstI[i]));

感谢大家的帮助。

【讨论】:

    【解决方案2】:

    这是一个好的开始。您可以再减少一项:

    //  Perform complex multiplication on the input and accumulate with the output
    pDstR[i] += fmsub(fSrc1R, fSrc2R, fSrc1I*fSrc2I);
    pDstI[i] += fmadd(fSrc1R, fSrc2I, fSrc2R*fSrc1I);
    

    这里你可以在虚部的计算中使用另一个fmadd

    pDstI[i] = fmadd(fSrc1R, fSrc2I, fmadd(fSrc2R, fSrc1I, pDstI[i]));
    

    同样你可以对实部做同样的事情,但你需要否定这个论点。如果这使事情变得更快或更慢,很大程度上取决于您正在处理的架构的微时序:

    pDstR[i] = fmsub(fSrc1R, fSrc2R, fmadd(fSrc1I, fSrc2I, -pDstR[i]));
    

    顺便说一句,如果您使用restrict 关键字将目标数组声明为非别名,您可能会获得进一步的性能改进。现在编译器必须假设 pDstR 和 pDstI 可能重叠或指向同一块内存。这将阻止编译器在写入 pDstR[i] 之前加载 pDstI[i]。

    之后,如果编译器还没有这样做,一些仔细的循环展开也可能会有所帮助。检查编译器的汇编输出!

    【讨论】:

    • 谢谢。我实际上能够独立地计算出虚部的第二个 fma。使用英特尔 FMA3 使用基于 8 宽 __m256 AVX 的循环,仅修改该行会导致 12% 的加速。然后,我尝试使用您的建议,即在加载后使用 xor 来否定真实目的地,但随后又放慢了速度。认为这是由于奇数异或的流水线不良造成的,我尝试不否定它并接受错误的结果,但性能仍然与以前相同。最终,看起来只是在虚部上使用第二个 fma 就赢了。
    • 此外,为目标指针添加限制关键字实际上会降低 msvc11 编译器的性能 20%。这是没有限制的 asm 输出:
    • vmovups ymm4,ymmword ptr [rax] vmovups ymm5,ymmword ptr [rax+r12] vmovups ymm0,ymmword ptr [rax+r15] lea rax,[rax+20h] vmulps ymm1,ymm4,ymmword ptr [rax+r9-20h] vfmsub231ps ymm1,ymm5,ymmword ptr [rax+r13-20h] vfmadd231ps ymm0,ymm4,ymmword ptr [rax+r13-20h] vaddps ymm1,ymm1,ymmword ptr [rax+r14-20h] vfmadd231ps ymm0,ymm5,ymmword ptr [rax+r9-20h] vmovups ymmword ptr [rax+r14-20h],ymm1 vmovups ymmword ptr [rax+r15-20h],ymm0
    • 并带有限制: vmovups ymm1,ymmword ptr [rax] vmovups ymm3,ymmword ptr [rax+r9] vmovups ymm4,ymmword ptr [rax+r12] vmovups ymm2,ymmword ptr [rax+r15] lea rax,[rax+20h] vfmadd231ps ymm2,ymm1,ymmword ptr [rax+r13-20h] vmulps ymm1,ymm1,ymm3 vfmadd231ps ymm2,ymm4,ymm3 vfmsub231ps ymm1,ymm4,ymmword ptr [rax+r13-20h] vaddps ymm1 ,ymm1,ymmword ptr [rax+r14-20h] vmovups ymmword ptr [rax+r14-20h],ymm1 vmovups ymmword ptr [rax+r15-20h],ymm2
    • 我不确定如何在此处将 cmets 格式化为代码块,但除了在限制版本中从内存移动后存在额外的寄存器使用之外,程序集非常相似,其中版本没有限制只是在第三个操作数而不是寄存器中使用相同的指针执行 vmulps,并且指令已经稍微重新排序。
    猜你喜欢
    • 2013-04-02
    • 2015-05-19
    • 2013-12-14
    • 1970-01-01
    • 2017-05-15
    • 1970-01-01
    • 2019-07-06
    • 2021-03-10
    • 2015-08-14
    相关资源
    最近更新 更多