Parallel.For()因重复执行而变慢.我该怎么看?

jrv*_*jrv 9 c# performance parallel.for

我在C#中写了一个天真的Parallel.For()循环,如下所示.我也使用常规for()循环来做同样的工作来比较单线程和多线程.每次运行它时,单线程版本大约需要五秒钟.并行版本最初花了大约三秒钟,但如果我运行它大约四次,它会急剧减速.大多数情况下大约需要30秒.有一次花了八十秒.如果我重新启动程序,并行版本将再次快速启动,但在三次或四次并行运行后减速.有时并行运行将再次加速到原来的三秒然后减速.

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

我应该了解哪些数据才能理解为什么我的第一个天真程序在经过多次运行后会变慢?Perfmon中有什么东西我应该看一下吗?我仍然怀疑它与内存有关,但我在定时器外部分配数组.我还在每次运行结束时尝试了一个GC.Collect(),但这似乎没有帮助,反正也不一致.可能是处理器某处的缓存对齐问题?我怎么知道这个?还有什么可能是原因吗?

JR

    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();

    }
Run Code Online (Sandbox Code Playgroud)

Alo*_*aus 8

我已经分析了你的代码,它确实看起来很奇怪.应该没有偏差.这不是分配问题(GC很好,每次运行只分配一个数组).

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

在x64上它工作正常,没有问题.只有x86版本似乎受其影响.我已经使用Windows性能工具包对其进行了分析,并发现它看起来像是一个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()
Run Code Online (Sandbox Code Playgroud)

似乎在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%
Run Code Online (Sandbox Code Playgroud)

在功能失调的情况下,大部分时间(50%)都花在clr.dll!COMInterlocked :: ExchangeAdd64上.此方法是使用FPO编译的,因为堆栈在中间被破坏以获得更高的性能.我认为Windows代码库中不允许使用此类代码,因为它会使分析更难.看起来优化已经走得太远了.当我单步调试器到实际的exachange操作时

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
Run Code Online (Sandbox Code Playgroud)

cmpxchg8b将EDX:EAX = 1c761bf与内存位置进行比较,如果值相等,则将ECX:EBX = 1c761cf的新值复制到内存位置.当您查看寄存器时,您会发现在索引0x1c761bf = 29.843.903处,所有值都不相等.在递增全局循环计数器时看起来存在竞争条件(或过度争用),仅当您的方法体执行如此少的工作以使其弹出时才会出现.

恭喜您在.NET Framework中发现了一个真正的错误!您应该在connect网站上报告,以使他们了解此问题.

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

    System.Diagnostics.Stopwatch s = new System.Diagnostics.Stopwatch();
    s.Start();
    Parallel.For(0,_len, i => {});
    s.Stop();
    System.Console.WriteLine(s.ElapsedMilliseconds.ToString());
Run Code Online (Sandbox Code Playgroud)

这也是一个问题.因此它肯定是一个CLR问题.通常我们在SO告诉人们不要尝试编写无锁代码,因为很难做到正确.但即使是MS中最聪明的人似乎也有时会弄错....

更新: 我在这里打开了一个错误报告:https://connect.microsoft.com/VisualStudio/feedbackdetail/view/969699/parallel-for-causes-random-slowdowns-in-x86-processes