并行化非常紧密的循环

lig*_*txx 5 c# parallel-processing performance multithreading parallel.foreach

我一直在敲打这个问题好几个小时,我总是因为线程争用而吃掉了并行化循环的性能改进.

我正在尝试计算8位灰度千兆像素图像的直方图.读过"CUDA by example"一书的人可能会知道它的来源(第9章).

该方法非常简单(导致非常紧凑的循环).它基本上就是这样

    private static void CalculateHistogram(uint[] histo, byte[] buffer) 
    {
        foreach (byte thisByte in buffer) 
        {
            // increment the histogram at the position
            // of the current array value
            histo[thisByte]++;
        }
    }
Run Code Online (Sandbox Code Playgroud)

其中buffer是1024 ^ 3个元素的数组.

在最新的Sandy Bridge-EX CPU构建中,10亿个元素的直方图在一个核心上运行1秒钟.

无论如何,我尝试通过在所有核心之间分配循环来加速计算,最终得到的解决方案慢了50倍.

    private static void CalculateHistrogramParallel(byte[] buffer, ref int[] histo) 
    {
        // create a variable holding a reference to the histogram array
        int[] histocopy = histo;

        var parallelOptions = new ParallelOptions { MaxDegreeOfParallelism = Environment.ProcessorCount };

        // loop through the buffer array in parallel
        Parallel.ForEach(
            buffer,
            parallelOptions,
            thisByte => Interlocked.Increment(ref histocopy[thisByte]));
    }
Run Code Online (Sandbox Code Playgroud)

很明显,由于原子增量的性能影响.

无论我尝试了什么(如范围分区程序[ http://msdn.microsoft.com/en-us/library/ff963547.aspx],并发集合[ http://msdn.microsoft.com/en-us/library/ dd997305(v = vs.110).aspx],等等)归结为我将十亿个元素减少到256个元素,并且在尝试访问我的直方图阵列时总是处于竞争状态.

我的最后一次尝试是使用范围分区器

       var rangePartitioner = Partitioner.Create(0, buffer.Length);

        Parallel.ForEach(rangePartitioner, parallelOptions, range => 
        {
            var temp = new int[256];
            for (long i = range.Item1; i < range.Item2; i++) 
            {
                temp[buffer[i]]++;
            }
        });
Run Code Online (Sandbox Code Playgroud)

计算子直方图.但最后,我仍然遇到问题,我必须合并所有这些子直方图,并再次爆炸,线程争用.

我拒绝相信没有办法通过并行化来加快速度,即使它是如此紧密的循环.如果它可能在GPU上,它必须 - 在某种程度上 - 也可以在CPU上.

除了放弃之外还有什么可以尝试的?

我搜索了stackoverflow和interwebs相当多,但这似乎是并行的边缘情况.

Chr*_*ris 2

我没有任何经验Parallel,但我用手动线程进行了测试,并且效果很好。

private class Worker
{
    public Thread Thread;
    public int[] Accumulator = new int[256];
    public int Start, End;
    public byte[] Data;

    public Worker( int start, int end, byte[] buf )
    {
        this.Start = start;
        this.End = end;
        this.Data = buf;

        this.Thread = new Thread( Func );
        this.Thread.Start();
    }
    public void Func()
    {
        for( int i = Start; i < End; i++ )
            this.Accumulator[this.Data[i]]++;
    }
}

int NumThreads = 8;
int len = buf.Length / NumThreads;

var workers = new Worker[NumThreads];
for( int i = 0; i < NumThreads; i++ )
    workers[i] = new Worker( i * len, i * len + len, buf );

foreach( var w in workers )
    w.Thread.Join();

int[] accumulator = new int[256];
for( int i = 0; i < workers.Length; i++ )
    for( int j = 0; j < accumulator.Length; j++ )
        accumulator[j] += workers[i].Accumulator[j];
Run Code Online (Sandbox Code Playgroud)

我的 Q720 移动 i7 上的结果:

Single threaded time = 5.50s
4 threads = 1.90s
8 threads = 1.24s
Run Code Online (Sandbox Code Playgroud)

看来它对我有用。有趣的是,尽管超线程核心共享缓存,8 个线程实际上比 4 个线程快一点。