隐式线程与显式线程性能

Implicit threading vs explicit threading performance

我正在用 C# 并行化一个应用程序,并且正在测试使用隐式线程与显式线程之间的性能差异。这两种技术都使用 System.Threading 库,隐式线程的特点是使用 Parallel.For 循环,而显式线程涉及创建、启动和加入线程,同时还计算块大小、调用辅助函数等.

我发现通过在八核上使用显式线程(在 50 次试验后大约快 1.2 倍),我比程序的原始顺序版本获得了更好的速度。我了解这两种技术之间的根本区别,但是,我不确定为什么显式版本似乎更快。我认为隐式版本可能会更快,因为任务会自动安排,而不是手动创建任务和线程。是否有理由(除了我的结果中可能有错误)显式版本会更快?

作为参考,下面是相关代码的摘要版本。

float[][] stft_implicit(Complex[] x, int wSamp)
{
    //...
    Parallel.For(0, size, new ParallelOptions { MaxDegreeOfParallelism = MainWindow.NUM_THREADS }, ii =>
    {
        Complex[] tempFFT = IterativeFFT.FFT(all_temps[ii], twiddles, wSamp);
        fft_results[ii] = tempFFT;
    });
    //...
}

float[][] stft_explicit(Complex[] x, int wSamp)
{
    //...
    length = (int)(2 * Math.Floor((double)N / (double)wSamp) - 1);
    chunk_size = (length + MainWindow.NUM_THREADS - 1) / MainWindow.NUM_THREADS;

    Thread[] threads = new Thread[MainWindow.NUM_THREADS];

    for (int i = 0; i < MainWindow.NUM_THREADS; i++)
    {
        threads[i] = new Thread(fft_worker);
        threads[i].Start(i);
    }

    for (int i = 0; i < MainWindow.NUM_THREADS; i++)
    {
        threads[i].Join();
    }
    //...
}

public void fft_worker(object thread_id)
{
    int ID = (int)thread_id;
    Complex[] temp = new Complex[wSamp];
    Complex[] tempFFT = new Complex[wSamp];
    int start = ID * chunk_size;
    int end = Math.Min(start + chunk_size, length);

    for (int ii = start; ii < end; ii++)
    {
        //...
        tempFFT = IterativeFFT.FFT(temp, twiddles, wSamp);
        //...
    }
}

我认为 Parallel.For 的比较不公平,因为它必须为处理数组的每个元素调用一个匿名 lambda,而显式线程实现涉及每个线程的单个方法调用( fft_worker 方法)。更重要的是 C# 编译器的匿名 lambdas cannot be inlined

要恢复比较的公平性,您可以:

  1. 在显式线程实现中也包括匿名 lambda 调用的开销:
for (int ii = start; ii < end; ii++)
{
    ((Action)(() =>
    {
        //...
        tempFFT = IterativeFFT.FFT(temp, twiddles, wSamp);
        //...
    }))();
}
  1. 通过将 Parallel.For 替换为 Parallel.ForEach+Partitioner 组合来减少隐式线程实现的粒度(或者换句话说增加块度):
Parallel.ForEach(Partitioner.Create(0, size), range =>
{
    for (int ii = range.Item1; ii < range.Item2; ii++)
    {
        Complex[] tempFFT = IterativeFFT.FFT(all_temps[ii], twiddles, wSamp);
        fft_results[ii] = tempFFT;
    }
});

我还没有测试过,但是这两个建议应该可以缩小或消除两种并行化技术在性能上的差距。