2013-08-22 69 views
2

我测试了一个互锁和一些其他的替代品。结果低于为什么互锁是如此之慢

ForSum: 16145,47 ticks 
ForeachSum: 17702,01 ticks 
ForEachSum: 66530,06 ticks 
ParallelInterlockedForEachSum: 484235,95 ticks 
ParallelLockingForeachSum: 965239,91 ticks 
LinqSum: 97682,97 ticks 
ParallelLinqSum: 23436,28 ticks 
ManualParallelSum: 5959,83 ticks 

如此互锁是甚至非平行linq的5倍,比parallelLinq慢20倍。它与“缓慢和丑陋的linq”相比。手动方法比它快几个数量级,我认为没有意义去比较它们。这怎么可能?如果这是真的,为什么我应该使用这个类而不是手动/ Linq并行求和?特别是如果目的是使用Linq我可以做的一切,而不是互锁,有可悲的数量的方法。

所以板凳代码是在这里:

using System; 
using System.Diagnostics; 
using System.Linq; 
using System.Reflection; 
using System.Runtime; 
using System.Runtime.CompilerServices; 
using System.Threading; 
using System.Threading.Tasks; 

namespace InterlockedTest 
{ 
    internal static class Program 
    { 
     private static void Main() 
     { 
      DoBenchmark(); 
      Console.ReadKey(); 
     } 

     private static void DoBenchmark() 
     { 
      Process.GetCurrentProcess().PriorityClass = ProcessPriorityClass.RealTime; 
      DisableGC(); 

      var arr = Enumerable.Repeat(6, 1005000*6).ToArray(); 
      int correctAnswer = 6*arr.Length; 

      var methods = new Func<int[], int>[] 
          { 
           ForSum, ForeachSum, ForEachSum, ParallelInterlockedForEachSum, ParallelLockingForeachSum, 
           LinqSum, ParallelLinqSum, ManualParallelSum 
          }; 

      foreach (var method in methods) 
      { 
       GC.Collect(); 
       GC.WaitForPendingFinalizers(); 
       GC.Collect(); 

       var result = new long[100]; 

       for (int i = 0; i < result.Length; ++i) 
       { 
        result[i] = TestMethod(method, arr, correctAnswer); 
       } 

       Console.WriteLine("{0}: {1} ticks", method.GetMethodInfo().Name, result.Average()); 
      } 
     } 

     private static void DisableGC() 
     { 
      GCLatencyMode oldMode = GCSettings.LatencyMode; 

      // Make sure we can always go to the catch block, 
      // so we can set the latency mode back to `oldMode` 
      RuntimeHelpers.PrepareConstrainedRegions(); 

      GCSettings.LatencyMode = GCLatencyMode.LowLatency; 
     } 

     private static long TestMethod(Func<int[], int> foo, int[] arr, int correctAnswer) 
     { 
      var watch = Stopwatch.StartNew(); 

      if (foo(arr) != correctAnswer) 
      { 
       return -1; 
      } 

      watch.Stop(); 
      return watch.ElapsedTicks; 
     } 

     private static int ForSum(int[] arr) 
     { 
      int res = 0; 

      for (int i = 0; i < arr.Length; ++i) 
      { 
       res += arr[i]; 
      } 

      return res; 
     } 

     private static int ForeachSum(int[] arr) 
     { 
      int res = 0; 

      foreach (var x in arr) 
      { 
       res += x; 
      } 

      return res; 
     } 

     private static int ForEachSum(int[] arr) 
     { 
      int res = 0; 

      Array.ForEach(arr, x => res += x); 

      return res; 
     } 

     private static int ParallelInterlockedForEachSum(int[] arr) 
     { 
      int res = 0; 

      Parallel.ForEach(arr, x => Interlocked.Add(ref res, x)); 

      return res; 
     } 

     private static int ParallelLockingForeachSum(int[] arr) 
     { 
      int res = 0; 
      object syncroot = new object(); 
      Parallel.ForEach(arr, i => 
            { 
             lock (syncroot) 
             { 
              res += i; 
             } 
            }); 
      return res; 
     } 

     private static int LinqSum(int[] arr) 
     { 
      return arr.Sum(); 
     } 

     private static int ParallelLinqSum(int[] arr) 
     { 
      return arr.AsParallel().Sum(); 
     } 

     static int ManualParallelSum(int[] arr) 
     { 
      int blockSize = arr.Length/Environment.ProcessorCount; 

      int blockCount = arr.Length/blockSize + arr.Length % blockSize; 

      var wHandlers = new ManualResetEvent[blockCount]; 

      int[] tempResults = new int[blockCount]; 

      for (int i = 0; i < blockCount; i++) 
      { 
       ManualResetEvent handler = (wHandlers[i] = new ManualResetEvent(false)); 

       ThreadPool.UnsafeQueueUserWorkItem(param => 
       { 
        int subResult = 0; 
        int blockIndex = (int)param; 
        int endBlock = Math.Min(arr.Length, blockSize * blockIndex + blockSize); 
        for (int j = blockIndex * blockSize; j < endBlock; j++) 
        { 
         subResult += arr[j]; 
        } 
        tempResults[blockIndex] = subResult; 

        handler.Set(); 
       }, i); 
      } 

      int res = 0; 

      for (int block = 0; block < blockCount; ++block) 
      { 
       wHandlers[block].WaitOne(); 
       res += tempResults[block]; 
      } 

      return res; 
     } 
    } 
} 

回答

3

的问题在这里是,它不得不为每个单独添加,这是一个巨大的开销量同步。

微软有provided a Partitioner class这基本上是为了提供您在ManualParallelSum()中使用的一些逻辑。

如果您使用Partitioner,它将大大简化代码,并且它大致在同一时间运行。

这里有一个简单的实现 - 如果你将它添加到您的测试程序,你应该会看到类似的结果ManualParallelSum()

private static int PartitionSum(int[] numbers) 
{ 
    int result = 0; 
    var rangePartitioner = Partitioner.Create(0, numbers.Length); 

    Parallel.ForEach(rangePartitioner, (range, loopState) => 
    { 
     int subtotal = 0; 

     for (int i = range.Item1; i < range.Item2; i++) 
      subtotal += numbers[i]; 

     Interlocked.Add(ref result, subtotal); 
    }); 

    return result; 
} 
+0

因此,我需要频繁切换临界 - 非临界部分,我可以手动使用分区? –

+1

@AlexJoukovsky当每次迭代完成的工作量非常小(如添加整数)时,您应该使用分区程序,以便每个线程在需要与其他线程同步之前执行更多的工作。 –

+0

我明白了。在这种情况下,Interlocked调用的计数等于'Environnment.ProcessorCount',不会更大。这是否意味着互锁可以作为天真的实现使用不是好主意,我可以尽可能多地编写手动代码,并尽量避免使用互锁? –

1

联锁和锁是快速操作时,不发生竞争。
在示例中,有很多争用,开销然后变得比底层操作(这是一个非常小的操作)重要得多。

Interlocked.Add即使没有并行性也会增加一个小的开销,但不会太多。

private static int InterlockedSum(int[] arr) 
{ 
    int res = 0; 

    for (int i = 0; i < arr.Length; ++i) 
    { 
     Interlocked.Add(ref res, arr[i]); 
    } 

    return res; 
} 

结果是: ForSum:6682.45蜱
InterlockedSum:15309.63蜱

与手动执行的比较看起来不公平,你分割块中的操作,因为你知道的性质操作。其他实现不能假定。

+0

这就是为什么我说我们不会与它比较。 PLINQ还没有这个信息,但工作正常。 –

+0

噢,没有看到。似乎PLINQ在InlinedAggregationOperator中有自己的分区逻辑(在PLINQ Sum运算符中使用它) –