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

时间:2014-09-11 17:29:43

标签: 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();

    }

2 个答案:

答案 0 :(得分: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()

似乎&#34;挂&#34;在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代码库中不允许使用此类代码,因为它会使分析更难。看起来优化已经走得太远了。 当我单步调试器到实际的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

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

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

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

答案 1 :(得分:2)

根据您的程序,我编写了一个程序来重现问题。我认为它与.NET大对象堆以及Parallel.For如何实现有关。

class Program
    {
        static void Main(string[] args)
        {
            for (int i = 0; i < 10; i++)
                //ParallelArray();
                SingleFor();
        }

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

         static void ParallelArray()
        {
            int[] stuff = new int[_meg];
            System.Diagnostics.Stopwatch s = new System.Diagnostics.Stopwatch();           
            s.Start();
            Parallel.For(0,
                _len,
                i =>
                {
                    stuff[i % _meg] = i;
                }
                );
            s.Stop();          

         System.Console.WriteLine( s.ElapsedMilliseconds.ToString());

        }

         static void SingleFor()
         {
             int[] stuff = new int[_meg];
             System.Diagnostics.Stopwatch s = new System.Diagnostics.Stopwatch();

             s.Start();

             for (int i = 0; i < _len; i++){
                     stuff[i % _meg] = i;
                 }

             s.Stop();            

             System.Console.WriteLine(s.ElapsedMilliseconds.ToString());
         }
    }

我使用VS2013编译,发布版本,并在没有调试器的情况下运行它。如果在主循环中调用ParallelArray()函数,我得到的结果是:

1631
1510
51302
1874
45243
2045
1587
1976
44257
1635

如果调用函数SingleFor(),结果为:

898
901
897
897
897
898
897
897
899
898

我在MSDN上查看关于Parallel.For的一些文档,this引起了我的注意:写入共享变量。如果循环体写入共享变量,则存在循环体依赖性。这是聚合值时发生的常见情况。与Parallel for循环一样,我们使用的是共享变量。

本文Parallel Aggregation解释了.NET如何处理这种情况:并行聚合模式使用在计算结束时合并的非共享局部变量来给出最终结果。对于部分的,局部计算的结果,使用非共享局部变量是循环的步骤如何彼此独立。并行聚合演示了这样的原则:对算法进行更改通常比将同步原语添加到现有算法更好。这意味着它创建了数据的本地副本,而不是使用锁来保护共享变量,最后,这10个分区需要组合在一起;这会带来性能损失。

当我使用Parall.For运行测试程序时,我使用进程探索计算线程,它有11个线程,所以Parallel.For为循环创建10个分区,这意味着它创建了10个大小的本地副本100K,这些对象将被放置在大对象堆上。

.NET中有两种不同类型的堆。小对象堆(SOH)和大对象堆(LOH)。如果对象大小大于85,000字节,则它在LOH中。在进行GC时,.NET以不同的方式处理2个堆。

正如本博客中所解释的:No More Memory Fragmentation on the .NET Large Object Heap堆之间的关键区别之一是SOH压缩内存,因此在LOH不采用压缩的情况下显着降低了内存碎片化的可能性。因此,过度使用LOH可能会导致内存碎片严重到导致应用程序出现问题。

当您分配大小&gt;的大数组时;连续85,000,当LOH陷入内存碎片时,性能下降。

如果您使用的是.NET 4.5.1,则可以将GCSettings.LargeObjectHeapCompactionMode设置为CompactOnce,以便在GC.Collect()之后使LOH紧凑。

另一篇了解此问题的好文章是:Large Object Heap Uncovered

需要进一步调查,但我现在没有时间。