测量Parallel.For的执行时间

时间:2012-10-15 11:25:04

标签: c# .net

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

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

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

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

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

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

有人有什么想法吗?

(请注意我需要实时估算parallel.for完成的时间,而不是总时间。我想让用户知道执行时还剩多少时间。

6 个答案:

答案 0 :(得分:3)

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

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

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

并行运行的每个循环将递增cnt。结果是cnt单调增加到n,而cnt/n是for完成量的百分比。由于cnt没有争用,因此没有并发问题,而且速度非常快且非常准确。

我们可以通过简单地计算For

来衡量执行期间任何时候并行cnt/n循环的完成百分比

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

显然,每项任务越不可预测,剩余计算时间就越不准确。这是预期的,并且通常没有解决方案(这就是为什么它被称为近似)。我们仍然可以完全准确地获得经过的计算时间或百分比。

“剩余时间”算法的任何估计的基本假设是每个子任务花费大致相同的计算时间(假设想要线性结果)。例如,如果我们有一个并行方法,其中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();
        }
    }
}

答案 1 :(得分:2)

这几乎是不可能回答的。

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

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

这就是预测事情需要多长时间才能发生极易出错的事实,实际上是徒劳无功。

答案 2 :(得分:1)

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

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

如果非常对于实现对预计运行时间的可靠估计非常重要,那么您可以设置一个custom partitioner,然后利用您对分区的了解来推断几个块的时间。一个帖子。

答案 3 :(得分:1)

这是一种可能的解决方案,用于衡量所有先前完成的任务的平均值。每个任务完成后,将调用Action<T>,您可以在其中汇总所有时间并将其除以完成的总任务。然而,这只是当前状态,无法预测任何未来的任务/平均值。 (正如其他人提到的,这很难)

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

     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]);
            }

答案 4 :(得分:0)

我建议在完成每个步骤报告时执行该方法。当然,这对于线程安全来说有点棘手,因此在实现时需要记住这一点。这将使您可以跟踪总数中已完成任务的数量,并且还可以(很容易地)知道每个步骤所花费的时间,这对于删除异常值等非常有用。

编辑:有些代码可以证明这个想法

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

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

答案 5 :(得分:0)

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

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