C# 并行化非常紧密的循环
在这个问题上我已经绞尽脑汁好几个小时了,我总是以线程争用消耗掉并行化循环的任何性能改进而告终 我试图计算8位灰度千兆像素图像的直方图。读过《CUDA by Example》一书的人可能知道这是从哪里来的(第9章) 该方法非常简单(导致非常紧密的循环)。基本上只是C# 并行化非常紧密的循环,c#,multithreading,performance,parallel-processing,parallel.foreach,C#,Multithreading,Performance,Parallel Processing,Parallel.foreach,在这个问题上我已经绞尽脑汁好几个小时了,我总是以线程争用消耗掉并行化循环的任何性能改进而告终 我试图计算8位灰度千兆像素图像的直方图。读过《CUDA by Example》一书的人可能知道这是从哪里来的(第9章) 该方法非常简单(导致非常紧密的循环)。基本上只是 private static void CalculateHistogram(uint[] histo, byte[] buffer) { foreach (byte thisByte in buffe
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]++;
}
}
其中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]));
}
很明显,因为原子增量的性能影响
无论我尝试了什么(如范围分割器[、并发集合[,等等),归根结底,我将10亿个元素减少到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]]++;
}
});
var rangePartitioner=Partitioner.Create(0,buffer.Length);
ForEach(rangePartitioner,parallelOptions,range=>
{
var temp=新整数[256];
对于(长i=range.Item1;i
计算子直方图。但最后,我仍然有一个问题,我必须合并所有这些子直方图,然后砰的一声,线程争用
我不相信并行化是没有办法加快速度的,即使它是一个如此紧密的循环。如果它在GPU上是可能的,那么在某种程度上,它在CPU上也是可能的
除了放弃,还有什么可以尝试的呢
我已经搜索了stackoverflow和interwebs,但这似乎是并行性的一个边缘案例。您应该使用一个具有本地状态的循环
并行化循环的每个单独分区都有一个唯一的局部状态,这意味着它不需要同步。作为最终操作,您必须将每个局部状态聚合为最终值。此步骤需要同步,但每个分区只调用一次,而不是每次迭代调用一次
而不是
Parallel.ForEach(
buffer,
parallelOptions,
thisByte => Interlocked.Increment(ref histocopy[thisByte]));
你可以用
Parallel.ForEach(
buffer,
parallelOptions,
() => new int[histocopy.Length], // initialize local histogram
(thisByte, state, local) => local[thisByte]++, // increment local histogram
local =>
{
lock(histocopy) // add local histogram to global
{
for (int idx = 0; idx < histocopy.Length; idx++)
{
histocopy[idx] += local[idx];
}
}
}
Parallel.ForEach(
缓冲器
平行选项,
()=>新建int[histocopy.Length],//初始化本地直方图
(thisByte,state,local)=>local[thisByte]+,//增量局部直方图
本地=>
{
lock(histocopy)//将局部直方图添加到全局直方图
{
for(intidx=0;idx
从分区大小和并行选项的默认选项开始并从中进行优化也可能是一个好主意。我没有任何关于
并行的经验,但我用手动线程进行了一次测试,效果非常好
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];
对我来说,这似乎是可行的。有趣的是,尽管超线程内核共享一个缓存,但8个线程实际上比4个线程快一点。我不知道这是否会更快,但有一点观察
如果对缓冲区[]中的所有元素进行排序会怎么样?这意味着不同内核之间不再存在交叉。如果性能适用,则可以增加内核计数,它应该线性增加。请注意,您确实需要更好地处理firstRange
/secondRange
拆分,因为您不希望在dif上有两个值相同的元素不同的范围
private static void CalculateHistogram(uint[] histo, byte[] buffer)
{
Array.Sort(buffer); // so the indexes into histo play well with cache.
// todo; rewrite to handle edge-cases.
var firstRange = new[] {0, buffer.Length/2}; // [inclusive, exclusive]
var secondRange = new[] {buffer.Length/2, buffer.Length};
// create two tasks for now ;o
var tasks = new Task[2];
var taskIdentifier = 0;
foreach (var range in new[] {firstRange, secondRange})
{
var rangeFix = range; // lambda capture ;s
tasks[taskIdentifier++] = Task.Factory.StartNew(() =>
{
for (var i = rangeFix[0]; i < rangeFix[1]; i++)
++histo[i];
});
}
Task.WaitAll(tasks);
}
private静态void CalculateHistogram(uint[]histo,byte[]buffer)
{
Array.Sort(buffer);//因此histo中的索引可以很好地使用缓存。
//todo;重写以处理边缘情况。
var firstRange=new[]{0,buffer.Length/2};//[包含,独占]
var secondRange=new[]{buffer.Length/2,buffer.Length};
//现在创建两个任务;o
var tasks=新任务[2];
var taskIdentifier=0;
foreach(新[]{firstRange,secondRange}中的变量范围)
{
var rangeFix=range;//lambda捕获;s
任务[taskIdentifier++]=Task.Factory.StartNew(()=>
{
对于(变量i=rangeFix[0];i
快速谷歌搜索告诉我,您可以使用C#&GPU对数字进行进一步排序,这将使性能提高约3倍,值得一试:
Ps有几个技巧可以带来非常可观的性能提升:
1) 记住错误缓存共享的概念-
2) 尝试使用stackalloc关键字,并确保所有内存分配都是通过堆栈完成的。相信我,任何内存分配都非常慢,除非直接从堆栈进行分配。我们讨论的是5倍的差异
3) 您可以使用C#MONO SIMD尝试对不同的数组求和(这是C版本,但该概念适用于C#)你有没有尝试过对每个并行的东西使用单独的历史
并在最后将它们全部相加?我做过类似的hough变换。我使用单独的累加器并在最后合并它们,给了我巨大的提升。在最后合并4/8个小数组应该不会是一个瓶颈。我个人从未使用过并行,所以不太了解这一点,但是如果你从中得不到提升,看起来它可能会做一些奇怪的事情。@ Lexxx考虑每个并行循环的启动成本,创建一个任务,分配一些L1/L2缓存,分配它认为需要的,引用内存等等。h是一个紧密的循环。您可以研究使用动态分区,并且通常会
private static void CalculateHistogram(uint[] histo, byte[] buffer)
{
Array.Sort(buffer); // so the indexes into histo play well with cache.
// todo; rewrite to handle edge-cases.
var firstRange = new[] {0, buffer.Length/2}; // [inclusive, exclusive]
var secondRange = new[] {buffer.Length/2, buffer.Length};
// create two tasks for now ;o
var tasks = new Task[2];
var taskIdentifier = 0;
foreach (var range in new[] {firstRange, secondRange})
{
var rangeFix = range; // lambda capture ;s
tasks[taskIdentifier++] = Task.Factory.StartNew(() =>
{
for (var i = rangeFix[0]; i < rangeFix[1]; i++)
++histo[i];
});
}
Task.WaitAll(tasks);
}