使用并行FOR循环节省时间
我有一个关于并行循环的问题。 我有以下代码:
public static void MultiplicateArray(double[] array, double factor) { for (int i = 0; i < array.Length; i++) { array[i] = array[i] * factor; } } public static void MultiplicateArray(double[] arrayToChange, double[] multiplication) { for (int i = 0; i < arrayToChange.Length; i++) { arrayToChange[i] = arrayToChange[i] * multiplication[i]; } } public static void MultiplicateArray(double[] arrayToChange, double[,] multiArray, int dimension) { for (int i = 0; i < arrayToChange.Length; i++) { arrayToChange[i] = arrayToChange[i] * multiArray[i, dimension]; } }
现在我尝试添加并行function:
public static void MultiplicateArray(double[] array, double factor) { Parallel.For(0, array.Length, i => { array[i] = array[i] * factor; }); } public static void MultiplicateArray(double[] arrayToChange, double[] multiplication) { Parallel.For(0, arrayToChange.Length, i => { arrayToChange[i] = arrayToChange[i] * multiplication[i]; }); } public static void MultiplicateArray(double[] arrayToChange, double[,] multiArray, int dimension) { Parallel.For(0, arrayToChange.Length, i => { arrayToChange[i] = arrayToChange[i] * multiArray[i, dimension]; }); }
问题是,我想节省时间,不要浪费它。 使用循环标准计算大约2分钟,但使用并行循环需要3分钟。 为什么?
Parallel.For()
可以通过对代码进行并行处理来提高性能,但同时也会产生开销(线程之间的同步,每次迭代调用委托)。 而且由于在你的代码中,每次迭代都很短(基本上只有几个CPU指令),这个开销可能会变得很突出。
因此,我认为使用Parallel.For()
不适合您。 相反,如果您手动并行化您的代码(在这种情况下非常简单),您可能会看到性能提高。
为了validation这一点,我进行了一些测量:我在200 000个项目的数组上运行了MultiplicateArray()
不同实现(我使用的代码如下)。 在我的机器上,序列版本一直花费0.21秒,而Parallel.For()
通常花了0.45秒左右,但是时不时地加速到8-9秒!
首先,我会尝试改善一些常见的情况,之后我会再次遇到这种情况。 我们想要用N个 CPU来处理数组,所以我们把它分成N个大小相等的部分,分别处理每个部分。 结果? 0.35秒 这比串行版本还要糟糕。 但是for
遍历数组中的每个项目是最优化的构造之一。 我们不能帮助编译器吗? 提取计算循环的边界可能会有所帮助。 事实certificate它确实:0.18秒。 这比串行版本更好,但不是太多。 有趣的是,在我的4核机器上(没有超线程)将并行度从4改为2并没有改变结果:仍然是0.18s。 这使我得出结论,CPU不是这里的瓶颈,内存带宽是。
现在,回到尖峰:我的自定义并行没有他们,但Parallel.For()
呢,为什么? Parallel.For()
确实使用了范围分区,这意味着每个线程都处理自己的数组部分。 但是,如果一个线程提前结束,它将尝试帮助处理尚未完成的另一个线程的范围。 如果发生这种情况,你会得到很多错误的分享,这可能会使代码变慢。 而我自己的强迫虚假分享的testing似乎表明这可能确实是问题所在。 强制Parallel.For()
的Parallel.For()
度似乎有助于尖峰一点。
当然,所有这些测量都是针对我电脑上的硬件而devise的,而且会因您而异,所以您应该自行测量。
我使用的代码:
static void Main() { double[] array = new double[200 * 1000 * 1000]; for (int i = 0; i < array.Length; i++) array[i] = 1; for (int i = 0; i < 5; i++) { Stopwatch sw = Stopwatch.StartNew(); Serial(array, 2); Console.WriteLine("Serial: {0:f2} s", sw.Elapsed.TotalSeconds); sw = Stopwatch.StartNew(); ParallelFor(array, 2); Console.WriteLine("Parallel.For: {0:f2} s", sw.Elapsed.TotalSeconds); sw = Stopwatch.StartNew(); ParallelForDegreeOfParallelism(array, 2); Console.WriteLine("Parallel.For (degree of parallelism): {0:f2} s", sw.Elapsed.TotalSeconds); sw = Stopwatch.StartNew(); CustomParallel(array, 2); Console.WriteLine("Custom parallel: {0:f2} s", sw.Elapsed.TotalSeconds); sw = Stopwatch.StartNew(); CustomParallelExtractedMax(array, 2); Console.WriteLine("Custom parallel (extracted max): {0:f2} s", sw.Elapsed.TotalSeconds); sw = Stopwatch.StartNew(); CustomParallelExtractedMaxHalfParallelism(array, 2); Console.WriteLine("Custom parallel (extracted max, half parallelism): {0:f2} s", sw.Elapsed.TotalSeconds); sw = Stopwatch.StartNew(); CustomParallelFalseSharing(array, 2); Console.WriteLine("Custom parallel (false sharing): {0:f2} s", sw.Elapsed.TotalSeconds); } } static void Serial(double[] array, double factor) { for (int i = 0; i < array.Length; i++) { array[i] = array[i] * factor; } } static void ParallelFor(double[] array, double factor) { Parallel.For( 0, array.Length, i => { array[i] = array[i] * factor; }); } static void ParallelForDegreeOfParallelism(double[] array, double factor) { Parallel.For( 0, array.Length, new ParallelOptions { MaxDegreeOfParallelism = Environment.ProcessorCount }, i => { array[i] = array[i] * factor; }); } static void CustomParallel(double[] array, double factor) { var degreeOfParallelism = Environment.ProcessorCount; var tasks = new Task[degreeOfParallelism]; for (int taskNumber = 0; taskNumber < degreeOfParallelism; taskNumber++) { // capturing taskNumber in lambda wouldn't work correctly int taskNumberCopy = taskNumber; tasks[taskNumber] = Task.Factory.StartNew( () => { for (int i = array.Length * taskNumberCopy / degreeOfParallelism; i < array.Length * (taskNumberCopy + 1) / degreeOfParallelism; i++) { array[i] = array[i] * factor; } }); } Task.WaitAll(tasks); } static void CustomParallelExtractedMax(double[] array, double factor) { var degreeOfParallelism = Environment.ProcessorCount; var tasks = new Task[degreeOfParallelism]; for (int taskNumber = 0; taskNumber < degreeOfParallelism; taskNumber++) { // capturing taskNumber in lambda wouldn't work correctly int taskNumberCopy = taskNumber; tasks[taskNumber] = Task.Factory.StartNew( () => { var max = array.Length * (taskNumberCopy + 1) / degreeOfParallelism; for (int i = array.Length * taskNumberCopy / degreeOfParallelism; i < max; i++) { array[i] = array[i] * factor; } }); } Task.WaitAll(tasks); } static void CustomParallelExtractedMaxHalfParallelism(double[] array, double factor) { var degreeOfParallelism = Environment.ProcessorCount / 2; var tasks = new Task[degreeOfParallelism]; for (int taskNumber = 0; taskNumber < degreeOfParallelism; taskNumber++) { // capturing taskNumber in lambda wouldn't work correctly int taskNumberCopy = taskNumber; tasks[taskNumber] = Task.Factory.StartNew( () => { var max = array.Length * (taskNumberCopy + 1) / degreeOfParallelism; for (int i = array.Length * taskNumberCopy / degreeOfParallelism; i < max; i++) { array[i] = array[i] * factor; } }); } Task.WaitAll(tasks); } static void CustomParallelFalseSharing(double[] array, double factor) { var degreeOfParallelism = Environment.ProcessorCount; var tasks = new Task[degreeOfParallelism]; int i = -1; for (int taskNumber = 0; taskNumber < degreeOfParallelism; taskNumber++) { tasks[taskNumber] = Task.Factory.StartNew( () => { int j = Interlocked.Increment(ref i); while (j < array.Length) { array[j] = array[j] * factor; j = Interlocked.Increment(ref i); } }); } Task.WaitAll(tasks); }
示例输出:
Serial: 0,20 s Parallel.For: 0,50 s Parallel.For (degree of parallelism): 8,90 s Custom parallel: 0,33 s Custom parallel (extracted max): 0,18 s Custom parallel (extracted max, half parallelism): 0,18 s Custom parallel (false sharing): 7,53 s Serial: 0,21 s Parallel.For: 0,52 s Parallel.For (degree of parallelism): 0,36 s Custom parallel: 0,31 s Custom parallel (extracted max): 0,18 s Custom parallel (extracted max, half parallelism): 0,19 s Custom parallel (false sharing): 7,59 s Serial: 0,21 s Parallel.For: 11,21 s Parallel.For (degree of parallelism): 0,36 s Custom parallel: 0,32 s Custom parallel (extracted max): 0,18 s Custom parallel (extracted max, half parallelism): 0,18 s Custom parallel (false sharing): 7,76 s Serial: 0,21 s Parallel.For: 0,46 s Parallel.For (degree of parallelism): 0,35 s Custom parallel: 0,31 s Custom parallel (extracted max): 0,18 s Custom parallel (extracted max, half parallelism): 0,18 s Custom parallel (false sharing): 7,58 s Serial: 0,21 s Parallel.For: 0,45 s Parallel.For (degree of parallelism): 0,40 s Custom parallel: 0,38 s Custom parallel (extracted max): 0,18 s Custom parallel (extracted max, half parallelism): 0,18 s Custom parallel (false sharing): 7,58 s
Svick已经提供了一个很好的答案,但是我想强调一点,关键不是“手动并行化你的代码”而不是使用Parallel.For()
但是你必须处理更大的数据块 。
这仍然可以使用Parallel.For()
像这样完成:
static void My(double[] array, double factor) { int degreeOfParallelism = Environment.ProcessorCount; Parallel.For(0, degreeOfParallelism, workerId => { var max = array.Length * (workerId + 1) / degreeOfParallelism; for (int i = array.Length * workerId / degreeOfParallelism; i < max; i++) array[i] = array[i] * factor; }); }
它与svicks CustomParallelExtractedMax()
相同,但更短,更简单,(在我的机器上)执行速度更快一些:
Serial: 3,94 s Parallel.For: 9,28 s Parallel.For (degree of parallelism): 9,58 s Custom parallel: 2,05 s Custom parallel (extracted max): 1,19 s Custom parallel (extracted max, half parallelism): 1,49 s Custom parallel (false sharing): 27,88 s My: 0,95 s
顺便说一句,这是从所有其他答案中缺less的关键字是粒度 。
Parallel.For涉及更复杂的内存pipe理。 该结果可能会有所不同,取决于cpu规格,如#cores,L1和L2caching…
请看看这个有趣的文章:
请参阅PLINQ和TPL的自定义分区程序 :
在
For
循环中,循环的主体作为委托提供给方法。 调用该委托的成本与虚拟方法调用大致相同。 在一些情况下,并行循环的主体可能足够小,以至于在每个循环迭代上委托调用的代价变得显着。 在这种情况下,可以使用Create
重载之一在源元素上创build一个IEnumerable<T>
范围分区。 然后,可以将这个范围集合传递给一个ForEach
方法,该方法的正文由一个常规的for循环组成。 这种方法的好处是委托调用的成本只发生在每个范围一次,而不是每个元素一次。
在你的循环体中,你正在执行一个单一的乘法,并且委托调用的开销将是非常明显的。
尝试这个:
public static void MultiplicateArray(double[] array, double factor) { var rangePartitioner = Partitioner.Create(0, array.Length); Parallel.ForEach(rangePartitioner, range => { for (int i = range.Item1; i < range.Item2; i++) { array[i] = array[i] * factor; } }); }
另请参见: Parallel.ForEach
文档和Partitioner.Create
文档 。
你并没有创build三个执行你的代码的线程/进程,但是for的迭代被尝试并行执行,所以即使只有一个代码使用了多个线程/进程。
这意味着索引= 0和索引= 1的交互可以同时执行。
可能的是,你迫使使用太多的线程/进程,并且创build/执行它们的开销比速度增加更大。
尝试使用三个正常的,但在三个不同的线程/过程中,如果您的系统是多核(至less3倍),应该不到一分钟