[英]Why interlocked is so slow
我已經測試了聯鎖和其他一些替代方案。 結果如下
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;
}
}
}
這里的問題是,每次添加時都必須同步,這是巨大的開銷。
Microsoft 提供了一個Partitioner
類 , 該類基本上旨在提供您在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;
}
當沒有爭用發生時,互鎖和鎖定是快速的操作。
在樣本中,存在很多爭用,因此開銷比基礎操作(這是一個很小的操作)要重要得多。
即使沒有並行性,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個滴答
互鎖總和:15309.63次
由於您知道操作的性質,因此將操作分成多個塊時,與手動實現的比較看起來並不公平。 其他實現不能假定這一點。
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.