【问题标题】:how would you optimize this vectorized sum of harmonics?你将如何优化这个矢量化的谐波总和?
【发布时间】:2021-01-17 21:08:19
【问题描述】:

我正在使用矢量化(仅 SSE2 max 作为 SIMD)将一组谐波相加,每个谐波具有不同的相位/幅度。

这是我的实际尝试:

float output = 0.0f;
simd::float_4 freqFundamentalNormalized = freq * (1.0f / sampleRate);
simd::float_4 harmonicIndex{1.0f, 2.0f, 3.0f, 4.0f};
simd::float_4 harmonicIncrement{4.0f, 4.0f, 4.0f, 4.0f};

// harmonics
const int numHarmonicsV4 = numHarmonics / 4;
const int numHarmonicsRemainder = numHarmonics - (numHarmonicsV4 * 4);

// v4
for (int i = 0; i < numHarmonicsV4; i++) {
    // signal
    simd::float_4 sineOutput4 = simd::sin(mPhases4[i] * g2PIf) * mMagnitudes4[i];

    for (int v = 0; v < 4; v++) {
        output += sineOutput4[v];
    }

    // increments
    mPhases4[i] += harmonicIndex * freqFundamentalNormalized;
    mPhases4[i] -= simd::floor(mPhases4[i]);

    harmonicIndex += harmonicIncrement;
}

// remainder
if (numHarmonicsRemainder > 0) {
    // signal
    simd::float_4 sineOutput4 = simd::sin(mPhases4[numHarmonicsV4] * g2PIf) * mMagnitudes4[numHarmonicsV4];

    for (int v = 0; v < numHarmonicsRemainder; v++) {
        output += sineOutput4[v];
    }

    // increments
    mPhases4[numHarmonicsV4] += harmonicIndex * freqFundamentalNormalized;
    mPhases4[numHarmonicsV4] -= simd::floor(mPhases4[numHarmonicsV4]);
}

但是:

  1. 我认为我可以对其进行更多优化,也许可以通过一些数学技巧,或者以一些增量进行保存
  2. 我不喜欢为V4 重复一次“相同的代码”,为remainder 重复一次(如果谐波数不是 % 4):有没有办法放置一种“掩码”到最后一个 V4 将(例如)幅度设置为 0? (因此它在同一个块中执行相同的操作,但不会对最终输出求和)。

【问题讨论】:

  • 水平求和到output inside 循环的成本很高。取而代之的是,在最后累积成一个和向量和 hsum 一次。 Fastest way to do horizontal SSE vector sum (or other reduction)
  • 计算以sin计算为主。有一些技巧可以迭代计算正弦 (sin(a+b) = ...),而无需调用 sin 函数。在这种情况下,你要注意舍入误差的累积,不时重新计算准确的正弦值。
  • 另外,sin 与您的其他代码相比非常昂贵,具体取决于您选择的精度与性能权衡(或您选择的库)。 (虽然floor 在没有 SSE4.1 的情况下是不平凡的;处理可能大量数字的通用版本可能很昂贵:Calculating floor & ceil of vector2 double using pre-SSE4。如果您不需要处理大量数字,请使用加/减来滚动您自己的地板大常数可以舍入,尽管 IIRC 这个技巧只直接给你当前的舍入模式(到最近,而不是朝向 -Inf)
  • 如果你有一个恒定的相位增量dphi,那么sin((n+1)dphi) = sin(n dphi) cos(dphi) + cos(n dphi) sin(dphi)cos(n+1)dphi 也一样。您只计算一次cos(dphi)sin(dphi) ...。如果原点有相位,则迭代公式仍然有效。我已经为多普勒生成使用了数十亿次这个技巧
  • 再次查看 Msalters 的回答。不需要直接调用exp(i*),而是实现迭代公式,用一个简单的复数乘法就可以了

标签: c++ optimization vectorization simd sse2


【解决方案1】:

问题的第二部分是最简单的。任何幅度为 0 的谐波都不会影响正弦输出,因此您只需将 mMagnitude 填充为 4 的倍数。

正如 Damien 所指出的,sin(x) 很昂贵。但由欧拉、exp(x)=cos(x) + i sin(x)exp(x+dx)==exp(x)*exp(dx)。每一步都只是一个复杂的乘法。

【讨论】:

  • 第二部分:如何更快地填充它? mMagnitudes4 是std::array&lt;simd::float_4, 32&gt; mMagnitudes4:假设我有 13 个谐波,我应该从 13 迭代到 15 并设置 0.0f?不确定它的效率:)
  • 我的意思是:你会做这样的事情吗? for (int i = 0; i &lt; numHarmonicsRemainder; i++) { mMagnitudes4[numHarmonicsV4][i] = 0.0f; }
  • @markzzz:您将mMagnitude[13][15] 设置为0.0f 一次,您可以在其中初始化其他mMagnitude 成员。您从显示的代码中消除了剩余部分,这对性能至关重要。这意味着所有数学现在都在 v4 部分中。更适合缓存,更适合分支预测。
  • 关于你最后的陈述,不确定我是否遵循数学。关于谐波求和的任何链接/资源?
  • @markzzz:您正确地注意到“sin(kx) + sin(zx) 不等于 sin (kx + zx)”。 Damien 确实有 sin (kx + zx) 的正确公式,尽管他使用了不同的符号。我倾向于不记得所有的 sin/cos 规则,而是使用欧拉公式来推导它们。例如。计算出示例 exp(x)*exp(dx)==(cos(x)+i sin(x)) * (cos(dx)+i sin(dx)) = cos(x)cos(dx)-sin(x)sin(dx) + i sin(x)cos(dx) + i cos(x) sin(dx) 让您回到 Damien 的公式。
【解决方案2】:

首先,确保simd::sin 的实现速度很快。请参阅XMVectorSin,尤其是XMVectorSinEstin DirectXMath library,了解如何制作一个快速的示例,或者从那里复制粘贴,或者包含库,它只是标题。指令集可通过预处理器宏进行切换,为了获得最佳性能,它需要 SSE 4.1 和 FMA3,但仅适用于 SSE2。

正如 cmets 中所说,在循环的所有迭代完成后,您应该只执行一次水平添加。在此之前,累积到 SIMD 向量中。

非常小,可能会被编译器优化,但你不应该像现在这样访问mPhases4。在循环体开始时将值加载到向量中,计算输出,递增,计算小数部分,每次迭代只存储一次更新的值。

【讨论】:

  • 是的,simd:sin 非常好用而且很快;)(即 sse_mathfun_sin_ps,对 dsp 很好)。关于最后一部分,你的意思是simd::float_4 phases4 = mPhases4[i];,使用阶段4,而不是mPhases4[i] = phases4;?当然可以,但我认为编译器已经为我做了这个。但我会这样写;)
  • @markzzz “sse_mathfun_sin_ps” 做了一个快速基准测试,XMVectorSin 使用 SSE2 快 16%,使用 SSE4 快 50%,使用 FMA3 快 109%。 XMVectorSinEst 甚至比这更快。 “关于最后一部分”是的,这正是我的意思。 “我认为编译器已经为我做了这件事”通常但并非总是如此,C++ 定义了一个内存模型,编译器只有在确定没有其他线程会看到该内存时才允许这样做。
  • @markzzz 顺便说一句,你会非常努力地找到一个工作状态不支持 SSE3 或 4.1 的 x86 设备。 SSE3 在 Pentium 4 的后续版本中出现,SSE 4.1 在 Core Solo/Duo 中引入,从那时起两者都得到普遍支持,包括 AMD CPU 和 Intel Atoms。
  • 这是我工作环境的具体情况......我自己无法决定:) 无论如何,并非所有 x64 处理器都支持 SSE4。我使用 -march=nocona 构建,但可能支持 SSE3...
  • @markzzz:尽管 Nocona 可以追溯到 2004 年,但它确实具有 SSE3。但是你真的需要支持 16 年的 CPU 吗?
猜你喜欢
  • 1970-01-01
  • 2023-03-27
  • 2018-08-19
  • 2021-01-18
  • 1970-01-01
  • 2013-08-27
  • 1970-01-01
  • 1970-01-01
  • 2013-01-05
相关资源
最近更新 更多