gpt4 book ai didi

c# - Parallel.For() 会随着重复执行而变慢。我应该看什么?

转载 作者:太空狗 更新时间:2023-10-29 17:48:03 26 4
gpt4 key购买 nike

我用 C# 编写了一个简单的 Parallel.For() 循环,如下所示。我还使用常规 for() 循环完成了相同的工作,以比较单线程与多线程。每次运行单线程版本大约需要五秒钟。并行版本一开始大约需要三秒钟,但如果我运行大约四次,速度就会急剧下降。通常需要大约三十秒。有一次需要八十秒。如果我重新启动程序,并行版本会再次快速启动,但在三四次并行运行后会变慢。有时并行运行会再次加速到原来的三秒,然后减速。

我编写了另一个 Parallel.For() 循环来计算 Mandelbrot 集成员(丢弃结果),因为我认为该问题可能与分配和操作大型数组的内存问题有关。第二个问题的 Parallel.For() 实现确实每次都比单线程版本执行得更快,而且时间也一致。

我应该查看哪些数据才能理解为什么我的第一个幼稚程序在多次运行后变慢了? Perfmon 中有什么我应该看的吗?我仍然怀疑它与内存有关,但我在定时器之外分配了数组。我还在每次运行结束时尝试了 GC.Collect() ,但这似乎没有帮助,反正不是始终如一。可能是处理器某处缓存的对齐问题?我怎么知道呢?还有其他可能的原因吗?

小红书

    const int _meg = 1024 * 1024;
const int _len = 1024 * _meg;

private void ParallelArray() {
int[] stuff = new int[_meg];
System.Diagnostics.Stopwatch s = new System.Diagnostics.Stopwatch();
lblStart.Content = DateTime.Now.ToString();
s.Start();

Parallel.For(0,
_len,
i => {
stuff[i % _meg] = i;
}
);
s.Stop();

lblResult.Content = DateTime.Now.ToString();

lblDiff.Content = s.ElapsedMilliseconds.ToString();

}

最佳答案

我分析了您的代码,它确实看起来很奇怪。不能有偏差。这不是分配问题(GC 很好,每次运行只分配一个数组)。

问题可以在我的 Haswell CPU 上重现,其中并行版本突然需要更长的时间来执行。我有 CLR 版本 4.0.30319.34209 FX452RTMGDR。

在 x64 上它工作正常并且没有问题。只有 x86 构建似乎受到影响。我已经使用 Windows Performance Toolkit 对其进行了概要分析,发现它看起来像是 TPL 尝试查找下一个工作项的 CLR 问题。有时碰巧调用

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 中发现了一个真正的错误!您应该在 connect 网站上报告,让他们知道这个问题。

要绝对确定这不是另一个问题,您可以使用空委托(delegate)尝试并行循环:

    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

关于c# - Parallel.For() 会随着重复执行而变慢。我应该看什么?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/25793658/

26 4 0
Copyright 2021 - 2024 cfsdn All Rights Reserved 蜀ICP备2022000587号
广告合作:1813099741@qq.com 6ren.com