【问题标题】:Why interlocked is so slow为什么联锁这么慢
【发布时间】:2013-08-22 11:33:44
【问题描述】:

我已经测试了联锁和其他一些替代方案。结果如下

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

所以 interlocked 比非并行 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;
        }
    }
}

【问题讨论】:

    标签: c# .net linq parallel-processing parallel.foreach


    【解决方案1】:

    这里的问题是每次添加都必须同步,这是一个巨大的开销。

    微软有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;
    }
    

    【讨论】:

    • 所以每次我需要在关键和非关键部分之间频繁切换时,我可以手动使用 Partitioner?
    • @AlexJoukovsky 当每次迭代完成的工作非常小(例如添加整数)时,您应该使用分区器,以便每个线程在需要同步之前执行更大量的工作其他线程。
    • 我明白了。在这种情况下,互锁调用的计数等于Environnment.ProcessorCount,而不是更大。这是否意味着 Interlocked 可以用作幼稚的实现不是一个好主意,我可以尽可能多地编写手动代码并尽量避免使用 interlocked?
    【解决方案2】:

    互锁和锁定是在不发生争用时的快速操作。
    在示例中,存在很多争用,因此开销变得比底层操作重要得多(这是一个非常小的操作)。

    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 滴答声

    当您将操作拆分为块时,与手动实现的比较看起来并不公平,因为您知道操作的性质。其他实现不能假设。

    【讨论】:

    • 这就是为什么我说我们不会与之比较。 PLINQ 也没有此信息,但工作正常。
    • 哦,是的,没看到。 PLINQ 似乎在 InlinedAggregationOperator 中有自己的分区逻辑(在 PLINQ Sum 运算符中使用)
    猜你喜欢
    • 2021-09-03
    • 2016-09-28
    • 2020-02-08
    • 2012-07-17
    • 2011-11-07
    • 2015-08-24
    • 2013-08-06
    • 2014-07-16
    • 2011-01-02
    相关资源
    最近更新 更多