问题 如何加速此循环(在C中)?


我正在尝试并行化C中的卷积函数。这是原始函数,它会卷积两个64位浮点数组:

void convolve(const Float64 *in1,
              UInt32 in1Len,
              const Float64 *in2,
              UInt32 in2Len,
              Float64 *results)
{
    UInt32 i, j;

    for (i = 0; i < in1Len; i++) {
        for (j = 0; j < in2Len; j++) {
            results[i+j] += in1[i] * in2[j];
        }
    }
}

为了允许并发(没有信号量),我创建了一个函数来计算特定位置的结果。 results 数组:

void convolveHelper(const Float64 *in1,
                    UInt32 in1Len,
                    const Float64 *in2,
                    UInt32 in2Len,
                    Float64 *result,
                    UInt32 outPosition)
{
    UInt32 i, j;

    for (i = 0; i < in1Len; i++) {
        if (i > outPosition)
            break;
        j = outPosition - i;
        if (j >= in2Len)
            continue;
        *result += in1[i] * in2[j];
    }
}

问题是,使用 convolveHelper 将代码减慢约3.5倍(在单个线程上运行时)。

关于如何加速的任何想法 convolveHelper,同时保持线程安全?


6269
2018-04-18 13:37


起源

当然 convolveHelper 比较慢您需要执行一定数量的多次转换和添加,以及一定数量的数组索引来计算。你添加的其他所有东西都需要更长的时间,就像第一次一样 if, j = ,第二个 if,更不用说功能进入/退出。 - Mike Dunlavey
迈克,我的问题不是“为什么这个实现更慢?”;很明显 convolveHelper 应该慢一点 在一个线程上。我重写函数的重点是让它并行运行,也许是使用CUDA。我问是否 convolveHelper 可以在保持线程安全的同时更有效地编写。 - splicer


答案:


时域中的卷积在傅立叶域中成为乘法。我建议你抓一个快速的FFT库(比如 FFTW并使用它。你将从O(n ^ 2)到O(n log n)。

算法优化几乎总是优于微优化。


10
2018-04-18 13:47



我知道,但我试图解决的问题需要在时域中进行卷积。 - splicer
是的,但Thomas的论点是,从输入数据的时域到频域的转换,使用O(n)中的乘法进行卷积然后转换回时域比优化O(n ^ 2)卷积更有效算法。您不必更改表示数据的方式,只需将其转换为执行卷积的一部分。 - Ernelli
是的,运行时间是一个问题。我看到人类是否可以区分不同音频卷积的结果。在测试之间等待半小时很烦人;) - splicer
@splicer - 在相关性中,将“泄漏”光谱相乘,然后再变换。泄漏是另一个域中时域波形的忠实表示。该算法将信号归零到newLen = 2 * max(in1Len,in2Len),计算FFT,乘以光谱(复数乘法),逆FFT,取第一个inInLen + in2Len-1样本作为答案。这就是Matlab / Octave在内部实现其相关函数的方式。 - mtrw
@splicer - 它可以在数学上显示,但不能由我显示:)这里有一条推理线,看看你是否觉得这令人信服:首先,假设你正在卷积的两个波形实际上是两个较长波形的周期脉冲的部分,脉冲之间有死区。其次,由于死区,线性卷积脉冲与循环卷积其周期性“父母”的一个周期相同。第三,每个信号的一个周期的DFT是精确的(纹波和泄漏以及所有),因此它们的乘积和逆变换也是精确的。希望有所帮助! - mtrw


可能有帮助的最明显的事情是预先计算循环的起始和结束索引,并删除额外的测试 i 和 j (及其相关的跳跃)。这个:

for (i = 0; i < in1Len; i++) {
   if (i > outPosition)
     break;
   j = outPosition - i;
   if (j >= in2Len)
     continue;
   *result += in1[i] * in2[j];
}

可以改写为:

UInt32 start_i = (in2Len < outPosition) ? outPosition - in2Len + 1 : 0;
UInt32 end_i = (in1Len < outPosition) ? in1Len : outPosition + 1;

for (i = start_i; i < end_i; i++) {
   j = outPosition - i;
   *result += in1[i] * in2[j];
}

这样,条件 j >= in2Len 从来都不是,循环测试基本上是测试的组合 i < in1Len 和 i < outPosition

从理论上讲,你也可以摆脱任务 j 然后转 i++ 成 ++i,但编译器可能已经为您做了那些优化。


2
2018-04-18 13:46



正是我在寻找的!谢谢泰勒:) - splicer
start_i 需要登录 outPosition - inLen 往往是负面的 - splicer
在那种情况下,那么真正应该做的是测试是否 outPosition > inLen 如果是这样的话 start_i = 0因为它没有意义 i 是否定的。 - Tyler McHenry
嗯..我只是尝试了你的代码,它不起作用:(我的确跑得很快...... - splicer
另外,请不要这么快就用“它不起作用”来解雇答案。这是你的程序,而不是我的程序,所以我不会花很多时间坐在这里并进行心理调试。该方法有效:预先计算循环边界。如果我以前用代数提出我的循环边界公式略有偏差,那么你应该能够找出并解决这个问题。 - Tyler McHenry


  • 而不是两个 if 循环中的语句,您可以计算正确的最小/最大值 i 在循环之前。

  • 您正在分别计算每个结果位置。相反,你可以拆分 results 数组到块并让每个线程计算一个块。块的计算看起来像 convolve 功能。


1
2018-04-18 13:45





除非您的数组非常大,否则使用线程实际上不太可能有用,因为启动线程的开销将大于循环的开销。但是,让我们假设您的阵列很大,并且线程是一个净胜利。在那种情况下,我会做以下事情:

  • 忘掉你的潮流 convolveHelper,这太复杂了,也无济于事。
  • 将循环内部拆分为螺纹功能。即只是做

    for (j = 0; j < in2Len; j++) {
        results[i+j] += in1[i] * in2[j];
    }
    

进入自己的功能 i 作为参数以及其他一切。

  • 有身体 convolve 只需启动线程。为了获得最大效率,请使用信号量以确保永远不会创建比核心更多的线程。

0
2018-04-18 13:49



是的,我的数组相当大(每个256KB)。你的方法的问题是 += 操作不是线程安全的:有很多组合 i 和 j 这可以产生一个给定的 outPosition。 - splicer


答案在于简单数学而非多线程(更新)


这就是为什么......

考虑 一个b + aC

你可以优化它 的a *(B + C) (一   多次繁殖

在你的情况下有 in2Len 不必要的乘法 内环。哪个可以消除。

因此,如下修改代码应该给我们reqd卷积:

注意: 以下代码返回 圆形卷积 必须展开才能获得 线性卷积 结果。

void convolve(const Float64 *in1,
              UInt32 in1Len,
              const Float64 *in2,
              UInt32 in2Len,
              Float64 *results)
{
    UInt32 i, j;

    for (i = 0; i < in1Len; i++) {

        for (j = 0; j < in2Len; j++) {
            results[i+j] += in2[j];
        }

        results[i] = results[i] * in1[i];

    }
}

这应该给U带来最大的性能跳跃。试试吧,看看!!

祝你好运!!

CVS @ 2600Hertz


0
2018-04-18 13:58



这将产生完全不同的结果。它不执行卷积。 - interjay
当然,但是当O(n log n)是一个解决方案时,你仍在优化O(n ^ 2)算法。当n增长时,O(n log n)总是超过O(n ^ 2)。也许你的数据不足以遇到这种转变,而O(n ^ 2)的微优化仍然可以改善结果。 - Ernelli
等一下......那不行! - splicer


我终于想出了如何正确预先计算开始/结束索引(两者给出的建议) 泰勒麦克亨利 和 interjay):

if (in1Len > in2Len) {
    if (outPosition < in2Len - 1) {
        start = 0;
        end = outPosition + 1;
    } else if (outPosition >= in1Len) {
        start = 1 + outPosition - in2Len;
        end = in1Len;
    } else {
        start = 1 + outPosition - in2Len;
        end = outPosition + 1;
    }
} else {
    if (outPosition < in1Len - 1) {
        start = 0;
        end = outPosition + 1;
    } else if (outPosition >= in2Len) {
        start = 1 + outPosition - in2Len;
        end = in1Len;
    } else {
        start = 0;
        end = in1Len;
    }
}

for (i = start; i < end; i++) {
    *result = in1[i] * in2[outPosition - i];
}

不幸的是,预先计算索引会产生 执行时间没有明显减少 :(


0
2018-04-18 19:01





让convolve助手在更大的集上工作,使用短外循环计算多个结果。

并行化的关键是在线程之间的工作分配之间找到一个很好的平衡点。不要使用比CPU核心数更多的线程。

在所有线程之间平均分配工作。有了这种问题,每个线程工作的复杂性应该是相同的。


-1
2018-04-18 13:47