我已经分析了您的代码,它确实看起来很奇怪。应该没有偏差。这不是分配问题(GC 很好,每次运行只分配一个数组)。
这个问题可以在我的 Haswell CPU 上重现,其中并行版本突然需要更长的时间来执行。我有 CLR 版本 4.0.30319.34209 FX452RTMGDR。
在 x64 上它工作正常并且没有问题。只有 x86 构建似乎受到它的影响。我使用 Windows Performance Toolkit 对其进行了分析,发现它看起来像是一个 CLR 问题,TPL 试图找到下一个工作项。有时会发生调用
System.Threading.Tasks.RangeWorker.FindNewWork(Int64 ByRef, Int64 ByRef)
System.Threading.Tasks.Parallel+<>c__DisplayClassf`1[[System.__Canon, mscorlib]].<ForWorker>b__c()
System.Threading.Tasks.Task.InnerInvoke()
System.Threading.Tasks.Task.InnerInvokeWithArg(System.Threading.Tasks.Task)
System.Threading.Tasks.Task+<>c__DisplayClass11.<ExecuteSelfReplicating>b__10(System.Object)
System.Threading.Tasks.Task.InnerInvoke()
似乎“挂”在 clr 本身。clr!COMInterlocked::ExchangeAdd64+0x4d
当我将采样堆栈与慢速和快速运行进行比较时,我发现:
ntdll.dll!__RtlUserThreadStart -52%
kernel32.dll!BaseThreadInitThunk -52%
ntdll.dll!_RtlUserThreadStart -52%
clr.dll!Thread::intermediateThreadProc -48%
clr.dll!ThreadpoolMgr::ExecuteWorkRequest -48%
clr.dll!ManagedPerAppDomainTPCount::DispatchWorkItem -48%
clr.dll!ManagedThreadBase_FullTransitionWithAD -48%
clr.dll!ManagedThreadBase_DispatchOuter -48%
clr.dll!ManagedThreadBase_DispatchMiddle -48%
clr.dll!ManagedThreadBase_DispatchInner -48%
clr.dll!QueueUserWorkItemManagedCallback -48%
clr.dll!MethodDescCallSite::CallTargetWorker -48%
clr.dll!CallDescrWorkerWithHandler -48%
mscorlib.ni.dll!System.Threading._ThreadPoolWaitCallback.PerformWaitCallback() -48%
mscorlib.ni.dll!System.Threading.Tasks.Task.System.Threading.IThreadPoolWorkItem.ExecuteWorkItem() -48%
mscorlib.ni.dll!System.Threading.Tasks.Task.ExecuteEntry(Boolean) -48%
mscorlib.ni.dll!System.Threading.Tasks.Task.ExecuteWithThreadLocal(System.Threading.Tasks.TaskByRef) -48%
mscorlib.ni.dll!System.Threading.ExecutionContext.Run(System.Threading.ExecutionContext System.Threading.ContextCallback System.Object Boolean) -48%
mscorlib.ni.dll!System.Threading.Tasks.Task.ExecutionContextCallback(System.Object) -48%
mscorlib.ni.dll!System.Threading.Tasks.Task.Execute() -48%
mscorlib.ni.dll!System.Threading.Tasks.Task.InnerInvoke() -48%
mscorlib.ni.dll!System.Threading.Tasks.Task+<>c__DisplayClass11.<ExecuteSelfReplicating>b__10(System.Object) -48%
mscorlib.ni.dll!System.Threading.Tasks.Task.InnerInvokeWithArg(System.Threading.Tasks.Task) -48%
mscorlib.ni.dll!System.Threading.Tasks.Task.InnerInvoke() -48%
ParllelForSlowDown.exe!ParllelForSlowDown.Program+<>c__DisplayClass1::<ParallelArray>b__0 -24%
ParllelForSlowDown.exe!ParllelForSlowDown.Program+<>c__DisplayClass1::<ParallelArray>b__0<itself> -24%
...
clr.dll!COMInterlocked::ExchangeAdd64 +50%
在功能失调的情况下,大部分时间 (50%) 都花在 clr.dll!COMInterlocked::ExchangeAdd64 中。此方法是使用 FPO 编译的,因为堆栈在中间被破坏以获得更高的性能。我认为 Windows 代码库中不允许使用此类代码,因为它使分析变得更加困难。看起来优化已经走得太远了。当我使用调试器单步执行实际的交换操作时
eax=01c761bf ebx=01c761cf ecx=00000000 edx=00000000 esi=00000000 edi=0274047c
eip=747ca4bd esp=050bf6fc ebp=01c761bf iopl=0 nv up ei pl zr na pe nc
cs=0023 ss=002b ds=002b es=002b fs=0053 gs=002b efl=00000246
clr!COMInterlocked::ExchangeAdd64+0x49:
747ca4bd f00fc70f lock cmpxchg8b qword ptr [edi] ds:002b:0274047c=0000000001c761bf
cmpxchg8b 将 EDX:EAX=1c761bf 与内存位置进行比较,如果值相等,则将 ECX:EBX=1c761cf 的新值复制到内存位置。当您查看寄存器时,您会发现索引 0x1c761bf = 29.843.903 所有值都不相等。看起来在增加全局循环计数器时存在竞争条件(或过度争用),只有当您的方法主体所做的工作太少以至于它弹出时才会出现。
恭喜您在 .NET Framework 中发现了一个真正的错误!您应该在连接网站上报告它,让他们意识到这个问题。
为了绝对确定这不是另一个问题,您可以尝试使用空委托的并行循环:
System.Diagnostics.Stopwatch s = new System.Diagnostics.Stopwatch();
s.Start();
Parallel.For(0,_len, i => {});
s.Stop();
System.Console.WriteLine(s.ElapsedMilliseconds.ToString());
这也重现了这个问题。因此,这绝对是一个 CLR 问题。通常我们在 SO 告诉人们不要尝试编写无锁代码,因为很难做到正确。但即使是 MS 最聪明的人,有时似乎也会弄错......
更新:
我在这里打开了一个错误报告:https ://connect.microsoft.com/VisualStudio/feedbackdetail/view/969699/parallel-for-causes-random-slowdowns-in-x86-processes