使用委托或lambda包装StopWatch计时?

98

我正在编写这样的代码,进行一些快速而简单的计时:

var sw = new Stopwatch();
sw.Start();
for (int i = 0; i < 1000; i++)
{
    b = DoStuff(s);
}
sw.Stop();
Console.WriteLine(sw.ElapsedMilliseconds);

肯定有一种方法可以将这段计时代码作为花哨的.NET 3.0 Lambda调用,而不是(上帝保佑)复制粘贴几次并将DoStuff(s)替换为DoSomethingElse(s)

我知道可以使用Delegate来完成,但我想知道Lambda的方式。

10个回答

134

如何扩展计时器(Stopwatch)类呢?

public static class StopwatchExtensions
{
    public static long Time(this Stopwatch sw, Action action, int iterations)
    {
        sw.Reset();
        sw.Start(); 
        for (int i = 0; i < iterations; i++)
        {
            action();
        }
        sw.Stop();

        return sw.ElapsedMilliseconds;
    }
}

然后像这样调用:

var s = new Stopwatch();
Console.WriteLine(s.Time(() => DoStuff(), 1000));

你可以添加另一个重载,省略 "iterations" 参数并使用一些默认值(比如 1000)调用此版本。


3
您可能希望用sw.StartNew()替换sw.Start(),以防止在重复调用s.Time()时意外增加经过的时间,因为会重复使用同一个Stopwatch实例。 - VVS
11
我同意使用 Enumerable.Range 的 "foreach" 看起来更加 "现代化",但我的测试显示,对于一个大数量的循环,它比一个 "for" 循环慢了约4倍。可能因情况而异。 - Matt Hamilton
2
-1:在这里使用类扩展没有意义。Time 表现为静态方法,丢弃 sw 中所有现有状态,因此将其引入为实例方法只是看起来花哨。 - ildjarn
2
@ildjam,感谢您留下评论解释您的投票理由,但我认为您误解了扩展方法背后的思想。 - Matt Hamilton
4
我认为不是这样的-它们是用来(logically)向现有类添加实例方法的。但是,这不比 Stopwatch.StartNew 更像是一个实例方法,后者之所以是静态的是有原因的。C#缺乏向现有类添加静态方法的能力(不像F#),因此我理解这种做法的冲动,但它仍然让我感到不舒服。 - ildjarn
显示剩余2条评论

37

以下是我一直在使用的内容:

public class DisposableStopwatch: IDisposable {
    private readonly Stopwatch sw;
    private readonly Action<TimeSpan> f;

    public DisposableStopwatch(Action<TimeSpan> f) {
        this.f = f;
        sw = Stopwatch.StartNew();
    }

    public void Dispose() {
        sw.Stop();
        f(sw.Elapsed);
    }
}

使用方法:

using (new DisposableStopwatch(t => Console.WriteLine("{0} elapsed", t))) {
  // do stuff that I want to measure
}

这是我见过的最佳解决方案!没有扩展(因此可以用于许多类)并且非常干净! - Calvin
我不确定我是否正确理解了用法示例。当我尝试在// do stuff that I want to measure下使用一些Console.WriteLine("")进行测试时,编译器根本不高兴。你应该在那里执行正常的表达式和语句吗? - Tim
@Tim - 我相信你已经解决了,但是 using 语句缺少一个括号。 - Alex

12

你可以尝试为你正在使用的类(或任何基类)编写扩展方法。

我会让调用看起来像这样:

Stopwatch sw = MyObject.TimedFor(1000, () => DoStuff(s));

接下来是扩展方法:

public static Stopwatch TimedFor(this DependencyObject source, Int32 loops, Action action)
{
var sw = new Stopwatch();
sw.Start();
for (int i = 0; i < loops; ++i)
{
    action.Invoke();
}
sw.Stop();

return sw;
}

任何继承自 DependencyObject 的对象现在都可以调用 TimedFor(..) 函数。该函数可以轻松地通过 ref 参数进行返回值的调整。

--

如果您不希望功能与任何类/对象绑定,您可以这样做:

public class Timing
{
  public static Stopwatch TimedFor(Action action, Int32 loops)
  {
    var sw = new Stopwatch();
    sw.Start();
    for (int i = 0; i < loops; ++i)
    {
      action.Invoke();
    }
    sw.Stop();

    return sw;
  }
}

然后你可以这样使用它:

Stopwatch sw = Timing.TimedFor(() => DoStuff(s), 1000);

如果前提无法达成,那么这个答案似乎具有一些不错的“通用”能力:

如何使用委托或lambda表达式包装StopWatch计时?


很酷,但我不喜欢它与特定类或基类的绑定方式;能否更通用地完成? - Jeff Atwood
在MyObject类中编写扩展方法?它可以轻松地更改为扩展Object类或继承树中的其他类。 - Mark Ingram
我想更偏向于静态的,不与任何特定的对象或类绑定..时间和时间控制是普遍存在的。 - Jeff Atwood
很好,第二个版本更符合我的想法,+1,但我将接受马特的回答,因为他先回复了。 - Jeff Atwood

7

StopWatch 类在发生错误时不需要被 DisposedStopped。因此,测量某个操作所需的最简代码为:

public partial class With
{
    public static long Benchmark(Action action)
    {
        var stopwatch = Stopwatch.StartNew();
        action();
        stopwatch.Stop();
        return stopwatch.ElapsedMilliseconds;
    }
}

示例调用代码

public void Execute(Action action)
{
    var time = With.Benchmark(action);
    log.DebugFormat(“Did action in {0} ms.”, time);
}

我不喜欢将迭代包含在StopWatch代码中的想法。您可以始终创建另一个处理执行N次迭代的方法或扩展。

public partial class With
{
    public static void Iterations(int n, Action action)
    {
        for(int count = 0; count < n; count++)
            action();
    }
}

示例调用代码

public void Execute(Action action, int n)
{
    var time = With.Benchmark(With.Iterations(n, action));
    log.DebugFormat(“Did action {0} times in {1} ms.”, n, time);
}

以下是扩展方法版本。
public static class Extensions
{
    public static long Benchmark(this Action action)
    {
        return With.Benchmark(action);
    }

    public static Action Iterations(this Action action, int n)
    {
        return () => With.Iterations(n, action);
    }
}

同时附上调用示例代码

public void Execute(Action action, int n)
{
    var time = action.Iterations(n).Benchmark()
    log.DebugFormat(“Did action {0} times in {1} ms.”, n, time);
}

我测试了静态方法和扩展方法(结合迭代和基准测试),预期执行时间与实际执行时间的差值小于等于1毫秒。


扩展方法版本让我垂涎三尺。 :) - bzlm

7
我一段时间前编写了一个简单的CodeProfiler类,它包装了Stopwatch以便使用Action轻松地对方法进行分析: http://www.improve.dk/blog/2008/04/16/profiling-code-the-easy-way 它还可以轻松地让您对多线程代码进行分析。以下示例将使用1-16个线程对操作lambda进行分析:
static void Main(string[] args)
{
    Action action = () =>
    {
        for (int i = 0; i < 10000000; i++)
            Math.Sqrt(i);
    };

    for(int i=1; i<=16; i++)
        Console.WriteLine(i + " thread(s):\t" + 
            CodeProfiler.ProfileAction(action, 100, i));

    Console.Read();
}

4
假设您只需要快速计时一件事情,这很容易使用。
  public static class Test {
    public static void Invoke() {
        using( SingleTimer.Start )
            Thread.Sleep( 200 );
        Console.WriteLine( SingleTimer.Elapsed );

        using( SingleTimer.Start ) {
            Thread.Sleep( 300 );
        }
        Console.WriteLine( SingleTimer.Elapsed );
    }
}

public class SingleTimer :IDisposable {
    private Stopwatch stopwatch = new Stopwatch();

    public static readonly SingleTimer timer = new SingleTimer();
    public static SingleTimer Start {
        get {
            timer.stopwatch.Reset();
            timer.stopwatch.Start();
            return timer;
        }
    }

    public void Stop() {
        stopwatch.Stop();
    }
    public void Dispose() {
        stopwatch.Stop();
    }

    public static TimeSpan Elapsed {
        get { return timer.stopwatch.Elapsed; }
    }
}

2

对于我来说,这个扩展在int上更加直观,你不再需要实例化一个Stopwatch或者担心重置它。

所以你现在有:

static class BenchmarkExtension {

    public static void Times(this int times, string description, Action action) {
        Stopwatch watch = new Stopwatch();
        watch.Start();
        for (int i = 0; i < times; i++) {
            action();
        }
        watch.Stop();
        Console.WriteLine("{0} ... Total time: {1}ms ({2} iterations)", 
            description,  
            watch.ElapsedMilliseconds,
            times);
    }
}

使用示例:

var randomStrings = Enumerable.Range(0, 10000)
    .Select(_ => Guid.NewGuid().ToString())
    .ToArray();

50.Times("Add 10,000 random strings to a Dictionary", 
    () => {
        var dict = new Dictionary<string, object>();
        foreach (var str in randomStrings) {
            dict.Add(str, null);
        }
    });

50.Times("Add 10,000 random strings to a SortedList",
    () => {
        var list = new SortedList<string, object>();
        foreach (var str in randomStrings) {
            list.Add(str, null);
        }
    });

示例输出:

Add 10,000 random strings to a Dictionary ... Total time: 144ms (50 iterations)
Add 10,000 random strings to a SortedList ... Total time: 4088ms (50 iterations)

2

您可以重载多个方法,以涵盖可能想要传递给Lambda的各种参数情况:

public static Stopwatch MeasureTime<T>(int iterations, Action<T> action, T param)
{
    var sw = new Stopwatch();
    sw.Start();
    for (int i = 0; i < iterations; i++)
    {
        action.Invoke(param);
    }
    sw.Stop();

    return sw;
}

public static Stopwatch MeasureTime<T, K>(int iterations, Action<T, K> action, T param1, K param2)
{
    var sw = new Stopwatch();
    sw.Start();
    for (int i = 0; i < iterations; i++)
    {
        action.Invoke(param1, param2);
    }
    sw.Stop();

    return sw;
}

如果必须返回一个值,您可以使用Func委托。如果每个迭代都必须使用唯一值,则还可以传入一个参数数组(或更多)。


1

我喜欢使用来自 Vance Morrison(.NET 性能专家之一)的 CodeTimer 类。

他在博客上发布了一篇名为“快速轻松地测量托管代码:CodeTimers”的文章。

其中包括很酷的东西,比如 MultiSampleCodeTimer。它可以自动计算平均值和标准差,而且非常容易打印出结果。


0
public static class StopWatchExtensions
{
    public static async Task<TimeSpan> LogElapsedMillisecondsAsync(
        this Stopwatch stopwatch,
        ILogger logger,
        string actionName,
        Func<Task> action)
    {
        stopwatch.Reset();
        stopwatch.Start();

        await action();

        stopwatch.Stop();

        logger.LogDebug(string.Format(actionName + " completed in {0}.", stopwatch.Elapsed.ToString("hh\\:mm\\:ss")));

        return stopwatch.Elapsed;
    }
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接