测量Parallel.For的执行时间

Posted

技术标签:

【中文标题】测量Parallel.For的执行时间【英文标题】:Measure Execution time of Parallel.For 【发布时间】:2012-10-15 11:25:03 【问题描述】:

我正在使用 Parallel.For 循环来提高计算的执行速度。

我想测量计算剩余的大致时间。通常,只需测量每一步所需的时间,并通过将步数乘以总步数来估算总时间。

例如,如果有 100 个步骤,而某个步骤需要 5 秒,那么除了总时间约为 500 秒外,其他步骤也可以。 (可以平均几个步骤并不断向用户报告这是我想要做的)。

我能想到的唯一方法是使用外部 for 循环,该循环基本上通过拆分 parallel.for 间隔并测量每个循环来恢复原始方式。

for(i;n;i += step)
    Time(Parallel.For(i, i + step - 1, ...))

这通常不是一个很好的方法,因为少数非常长的步骤或大量的短步骤都会导致计时问题。

有人有什么想法吗?

(请注意,我需要实时估计完成 parallel.for 所花费的时间,而不是总时间。我想让用户知道执行过程中还剩多少时间)。

【问题讨论】:

这是一个难题。见xkcd.com/612 【参考方案1】:

这个方法似乎很有效。我们可以通过简单地让每个并行循环增加一个计数器来“线性化”并行 for 循环:

Parallel.For(0, n, (i) =>  Thread.Sleep(1000); Interlocked.Increment(ref cnt); );     

(请注意,感谢 Niclas,++ 不是原子的,必须使用 lockInterlocked.Increment

每个并行运行的循环都会增加cnt。效果是cnt 单调递增到n,而cnt/n 是for 完成的百分比。由于cnt 不存在争用,因此不存在并发问题,并且非常快速且非常准确。

我们可以通过简单地计算cnt/n来测量执行过程中任何时候并行For循环的完成百分比

总计算时间可以很容易地通过将循环开始以来经过的时间除以循环所处的百分比来估算。当每个循环所花费的时间大致相同时,这两个量应该具有大致相同的变化率并且表现相对良好(也可以平均小波动)。

显然每个任务越不可预测,剩余的计算时间就越不准确。这是意料之中的,一般来说,没有解决方案(这就是为什么它被称为近似值)。我们仍然可以完全准确地获得经过的计算时间或百分比。

任何“剩余时间”算法估计的基本假设是每个子任务需要大约相同的计算时间(假设一个想要线性结果)。例如,如果我们有一个并行方法,其中 99 个任务非常快,1 个任务非常慢,我们的估计将非常不准确。我们的计数器会很快拉到 99,然后坐在最后一个百分比,直到缓慢的任务完成。我们可以进行线性插值并进行进一步估计以获得更平滑的倒计时,但最终会有一个突破点。

以下代码演示了如何有效地测量并行度。注意 100% 的时间是真正的总执行时间,可以作为参考。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Diagnostics;

namespace ParallelForTiming

    class Program
           
        static void Main(string[] args)
        
            var sw = new Stopwatch();
            var pct = 0.000001;
            var iter = 20;
            var time = 20 * 1000 / iter;
            var p = new ParallelOptions(); p.MaxDegreeOfParallelism = 4;

            var Done = false;
            Parallel.Invoke(() =>
            
                sw.Start();
                Parallel.For(0, iter, p, (i) =>  Thread.Sleep(time); lock(p)  pct += 1 / (double)iter; );               
                sw.Stop(); 
                Done = true;

            , () =>
            
                while (!Done)
                
                    Console.WriteLine(Math.Round(pct*100,2) + " : " + ((pct < 0.1) ? "oo" : (sw.ElapsedMilliseconds / pct /1000.0).ToString()));
                    Thread.Sleep(2000);
                

            
            );
            Console.WriteLine(Math.Round(pct * 100, 2) + " : " + sw.ElapsedMilliseconds / pct / 1000.0);


            Console.ReadKey();
        
    

【讨论】:

Okey.. 这不正是 Alex 建议的一种实现方式吗?另外,可能只是我今天早上没有喝足够的咖啡,但那里有锁吗?看起来你只是添加到一个全局变量而不先锁定它......?如果您想按照这些思路做一些事情,我会改用 Alex 的实现。 IMO 更清洁。 @Niclas 不一样,但很接近。他计算每个任务的时间。必须比递增计数器慢。因为没有争用,所以不需要在柜台上锁。计数器不可能损坏,因为它是单原子操作。您肯定意识到 sleep 只是用于演示目的,并且是任何一般计算的占位符吗?您还意识到我的代码大约是 Alex 的 1/10,本质上只是在基本案例中添加了两行代码?我不明白你怎么能说他“更干净”,但我猜每个人都有自己的想法。 顺便说一句,我的版本基于一个非常简单的计算百分比原理,并且非常准确。亚历克斯的每一项任务的时间都是不可预测的。一项任务可能会被另一项任务打断,从而导致时间被夸大。 自从讨论得到清理(我猜是正确的)看来,这是对该问题的正确答案。它不是。此代码不是线程安全的,任何寻求此问题解决方案的人都不应使用此代码。考虑这个简短的例子:- int count = 1; Parallel.For(0, 20000000, (i) =&gt; count++; ); Console.WriteLine(count.ToString()); Console.ReadKey(); 我做了,有人删除了所有这些 cmets。这是我第一次发表评论时所追求的,但我没有时间逐行浏览所有内容,并且觉得你在双线程和睡眠方面做了一些非常棘手的事情,而不是仅仅回答你开始抨击我不“知道”睡眠是为了什么......这真的让我很生气。【参考方案2】:

这几乎是不可能回答的。

首先,不清楚所有步骤的作用。有些步骤可能是 I/O 密集型或计算密集型的。

此外,Parallel.For 是一个请求——您不确定您的代码是否会真正并行运行。代码是否真正并行运行取决于环境(线程和内存的可用性)。然后,如果您有依赖 I/O 的并行代码,则一个线程将在等待 I/O 完成时阻塞其他线程。而且你也不知道其他进程在做什么。

这就是为什么预测某件事需要多长时间非常容易出错,而且实际上是徒劳的。

【讨论】:

哦,来吧,它一直都在做!在大多数情况下,大多数过程都是相当可预测的,有一些想法总比没有好。当你对许多小的快速步骤进行平均时,它往往是相当准确的。你所做的只是列出了我提到的问题。就我而言,我正在计算同一数学函数的不同部分,因此所有并行部分将在大约同一时间执行。有一个在某些情况下有效的解决方案总比没有解决方案要好,因为它可能无法在所有情况下都很好地工作。 一直都是这样,是的——很糟糕。查看 Access、Windows 文件副本或汽车中的 GPS 的预测。只有当您的流程的所有步骤都非常可预测并且您的机器没有执行任何其他需要大量资源或 I/O 的操作时,这才能可靠地完成。 即使他们不是最好有一些想法。我宁愿估计一个漫长的过程需要的时间,即使它偏离了 10 倍,也不愿不知道需要多长时间。无论如何,大多数涉及计算的东西都是可以预测的。我对你的陈述的主要问题是你的行为就像因为有些事情没有准确估计(这可能可以通过统计分析和性能收集来改进)我们应该抛弃整个想法或者它“一直”很糟糕.肯定有这样的情况。【参考方案3】:

这个问题很难回答。您提到的使用非常长的步骤或大量非常短的步骤的时间问题可能与您的循环将在并行分区器可以处理的边缘工作有关。

由于默认分区器是非常动态的,而且我们对您的实际问题一无所知,因此没有好的答案可以让您解决手头的问题,同时仍然可以通过动态负载平衡获得并行执行的好处。

如果实现对预计运行时间的可靠估计非常很重要,也许您可​​以设置一个custom partitioner,然后利用您对分区的了解从一个线程上的几个块推断时间.

【讨论】:

自定义分区器似乎肯定会解决问题,但仅从最初的潜在解决方案中删除了一步(使用外部 for 循环)。我认为我有一种方法可以有效地做到这一点,实际上非常简单,但需要对其进行测试(至少给出一个百分比,但将其转换为时间应该相当容易。【参考方案4】:

这里有一个可能的解决方案来衡量所有以前完成的任务的平均值。每个任务完成后,会调用Action&lt;T&gt;,您可以在其中总结所有时间并将其除以完成的总任务。然而,这只是当前状态,无法预测任何未来的任务/平均值。 (正如其他人提到的,这非常困难)

但是:您必须衡量它是否适合您的问题,因为在两个方法级别声明的变量上都有可能发生锁争用。

     static void ComputeParallelForWithTLS()
            
                var collection = new List<int>()  1000, 2000, 3000, 4000 ; // values used as sleep parameter
                var sync = new object();
                TimeSpan averageTime = new TimeSpan();
                int amountOfItemsDone = 0; // referenced by the TPL, increment it with lock / interlocked.increment

                Parallel.For(0, collection.Count,
                    () => new TimeSpan(),
                    (i, loopState, tlData) =>
                    
                        var sw = Stopwatch.StartNew();
                        DoWork(collection, i);
                        sw.Stop();
                        return sw.Elapsed;
                    ,
                    threadLocalData =>   // Called each time a task finishes
                    
                        lock (sync)
                        
                            averageTime += threadLocalData; // add time used for this task to the total.
                        
                        Interlocked.Increment(ref amountOfItemsDone); // increment the tasks done
                        Console.WriteLine(averageTime.TotalMilliseconds / amountOfItemsDone + ms."); 
/*print out the average for all done tasks so far. For an estimation, 
multiply with the remaining items.*/
                    );
            
            static void DoWork(List<int> items, int current)
            
                System.Threading.Thread.Sleep(items[current]);
            

【讨论】:

这在性能方面是一个非常糟糕的实现,因为每个线程都会锁定并且必须等待所有其他线程访问共享变量,只是为了显示进度指示器。如果工作被分成许多小块并在许多不同的线程之间进行分区,这种方法尤其糟糕。 @Niclas 虽然我同意访问共享变量是有代价的,但锁争用仅在每个任务完成其工作后才会发生(并且仅当两个或多个任务同时完成时)。所以你不能笼统地说这将是一个问题。您必须在您的确切用例中进行衡量。 是的,你完全正确。当我说这是一个非常糟糕的实现时,也许我有点苛刻。然而,这是一个有风险的实现,因为您无法自己控制任务的划分和线程。因此,它可能会完全扼杀性能,或者根本无法改变性能,而且实际上无法确定您是否有可能在运行时发生变化的动态问题。如果不了解手头的问题,我不会推荐这种实现方式。 由于正确处理多线程确实很难,因此程序员应该始终衡量可能的解决方案。我只是提供了一个使用 TPL 中的线程本地数据的想法,因为您可以在每个任务完成后挂钩。 来自计算科学背景,我同意和不同意你的观点。当然,您应该尽可能地进行测量,但 parallel.for 的全部意义在于减轻程序员的并行化负担,因此您无法控制并行化的执行方式,然后进行测量以改进变得毫无意义。如果问题的性质可能会在运行时发生变化,那将变得更加毫无意义,因为 parallel.for 可能会根据问题甚至特定硬件具有非常不同的行为 - 您不能对此做出任何假设......【参考方案5】:

我建议在完成后在每个步骤报告中执行该方法。当然,这对于线程安全来说有点棘手,因此在实现时要记住这一点。这将使您可以跟踪完成任务的总数,并且还可以(某种程度上)轻松了解每个单独步骤所花费的时间,这对于删除异常值等很有用。

编辑:一些代码来展示这个想法

Parallel.For(startIdx, endIdx, idx => 
    var sw = Stopwatch.StartNew();
    DoCalculation(idx);
    sw.Stop();
    var dur = sw.Elapsed;
    ReportFinished(idx, dur);
);

这里的关键是ReportFinished 将为您提供有关已完成任务数量以及每个任务持续时间的连续信息。这使您可以通过对这些数据进行统计来更好地猜测剩余时间。

【讨论】:

是的,这是基本思想,但必须知道将执行多少任务并以适当的方式将它们分开。我希望有人知道如何为 Parallel.For 获得这些结果。 @Archival:你不需要将它们分成块,我会说(你确实需要知道有多少,否则很难知道还剩下多少)。我将编辑一些代码来说明我的意思。 嗯,我的意思是,“除以”是您必须知道如何为它们计时。 Parallel.For 在内部执行此操作。我发现您可以简单地计算每个任务并通过知道总数(传递给 Parallel.For,很容易获得完成百分比,并且可以从中估算总时间。请参阅我的答案以获取有效的解决方案(目前看来效果不错)。【参考方案6】:

在这里我写了一个测量时间和速度的课程

public static class Counter

    private static long _seriesProcessedItems = 0;
    private static long _totalProcessedItems = 0;
    private static TimeSpan _totalTime = TimeSpan.Zero;
    private static DateTime _operationStartTime;
    private static object _lock = new object();
    private static int _numberOfCurrentOperations = 0;



    public static void StartAsyncOperation()
    
        lock (_lock)
        
            if (_numberOfCurrentOperations == 0)
            
                _operationStartTime = DateTime.Now;   
            

            _numberOfCurrentOperations++;
        
    

    public static void EndAsyncOperation(int itemsProcessed)
    
        lock (_lock)
        
            _numberOfCurrentOperations--;
            if (_numberOfCurrentOperations < 0) 
                throw new InvalidOperationException("EndAsyncOperation without StartAsyncOperation");

            _seriesProcessedItems +=itemsProcessed;

            if (_numberOfCurrentOperations == 0)
            
                _totalProcessedItems += _seriesProcessedItems;
                _totalTime += DateTime.Now - _operationStartTime;
                _seriesProcessedItems = 0;
            
        
    

    public static double GetAvgSpeed()
    
        if (_totalProcessedItems == 0) throw new InvalidOperationException("_totalProcessedItems is zero");
        if (_totalProcessedItems == 0) throw new InvalidOperationException("_totalTime is zero");
        return _totalProcessedItems / (double)_totalTime.TotalMilliseconds;
    

    public static void Reset()
    
        _totalProcessedItems = 0;
        _totalTime = TimeSpan.Zero;
    

使用和测试示例:

    static void Main(string[] args)
    
        var st = Stopwatch.StartNew();
        Parallel.For(0, 100, _ =>
        
            Counter.StartAsyncOperation();
            Thread.Sleep(100);
            Counter.EndAsyncOperation(1);
        );

        st.Stop();
        Console.WriteLine("Speed correct 0", 100 / (double)st.ElapsedMilliseconds);

        Console.WriteLine("Speed to test 0", Counter.GetAvgSpeed());
    

【讨论】:

以上是关于测量Parallel.For的执行时间的主要内容,如果未能解决你的问题,请参考以下文章

是否可以在 Parallel.For 中定义执行顺序?

c# 异步编程学习笔记

c# 异步编程学习笔记

c# 异步编程学习笔记

c# 异步编程学习笔记

在Parallel.For中,是否可以同步每个线程?