如何衡量.NET中的代码性能?

How to measure code performance in .NET?


我正在使用DateTime对一行C#代码进行一些真正的快速和脏的基准测试:

1
2
3
long lStart = DateTime.Now.Ticks;
// do something
long lFinish = DateTime.Now.Ticks;


问题在于结果:

1
2
3
4
5
6
7
8
Start Time [633679466564559902]
Finish Time [633679466564559902]

Start Time [633679466564569917]
Finish Time [633679466564569917]

Start Time [633679466564579932]
Finish Time [633679466564579932]


...等等。


鉴于开始和结束时间相同,Ticks显然不够精细。


那么,我怎样才能更好地衡量绩效呢?



从.NET 2.0开始提供的Stopwatch类是最好的方法。它是一个非常高性能的计数器,精确到几毫秒。
看看MSDN文档,这很清楚。


编辑:如前所述,建议多次运行代码以获得合理的平均时间。



重复执行您的代码。问题似乎是您的代码执行速度比测量仪器的粒度快得多。对此最简单的解决方案是执行许多次(数千次,数百万次)代码,然后计算平均执行时间。


编辑:此外,由于当前优化编译器(以及CLR和JVM等虚拟机)的性质,测量单行代码的执行速度可能会非常误导,因为测量可能会对速度产生很大的影响。更好的方法是分析整个系统(或至少更大的块)并检查瓶颈在哪里。



我觉得这些很有用


http://accelero.codeplex.com/SourceControl/changeset/view/22633#290971
http://accelero.codeplex.com/SourceControl/changeset/view/22633#290973
http://accelero.codeplex.com/SourceControl/changeset/view/22633#290972


TickTimer是Stopwatch的缩减副本,在构建时启动,不支持重新启动。如果当前硬件不支持高分辨率计时,它也会通知您(秒表吞下此问题)


所以这

1
2
3
4
5
6
7
8
var tickTimer = new TickTimer();
//call a method that takes some time
DoStuff();
tickTimer.Stop();
Debug.WriteLine("Elapsed HighResElapsedTicks" + tickTimer.HighResElapsedTicks);
Debug.WriteLine("Elapsed DateTimeElapsedTicks" + tickTimer.DateTimeElapsedTicks);
Debug.WriteLine("Elapsed ElapsedMilliseconds" + tickTimer.ElapsedMilliseconds);
Debug.WriteLine("Start Time" + new DateTime(tickTimer.DateTimeUtcStartTicks).ToLocalTime().ToLongTimeString());


会输出这个

1
2
3
4
Elapsed HighResElapsedTicks 10022886
Elapsed DateTimeElapsedTicks 41896
Elapsed ElapsedMilliseconds 4.18966178849554
Start Time 11:44:58


DebugTimer是TickTimer的包装器,它将结果写入Debug。 (注意:它支持一次性模式)


所以这

1
2
3
4
5
using (new DebugTimer("DoStuff"))
{
    //call a method that takes some time
    DoStuff();
}


将它输出到调试窗口

1
DoStuff: Total 3.6299 ms


IterationDebugTimer用于计算多次运行操作并将结果写入Debug所需的时间。它还将执行未包含的初始运行,以忽略启动时间。 (注意:它支持一次性模式)


所以这

1
2
3
4
5
6
7
8
int x;
using (var iterationDebugTimer = new IterationDebugTimer("Add", 100000))
{
    iterationDebugTimer.Run(() =>
    {
        x = 1+4;
    });
}


会输出这个

1
2
3
Add: Iterations 100000
Total 1.198540 ms
Single 0.000012 ms


只是添加其他人已经说过的关于使用秒表和测量平均值的内容。


确保在测量前调用您的方法。否则,您将测量JIT编译代码所需的时间。这可能会显着扭曲您的数字。


此外,请确保测量发布模式代码,因为默认情况下调试版本的优化已关闭。调整调试代码是毫无意义的。


并确保您正在测量您实际想要测量的内容。当优化启动时,编译器/ JIT编译器可能会重新排列代码或将其完全删除,因此您最终可能会测量与预期稍有不同的内容。至少看看生成的代码,以确保代码没有被剥离。


根据您要测量的内容,请记住,真实系统会比典型的测试应用程序更不同于运行时。一些性能问题与例如对象如何被垃圾收集。这些问题通常不会出现在简单的测试应用程序中。


实际上,最好的建议是使用真实数据来测量真实系统,因为沙箱测试可能会变得非常不准确。



使用真实的分析器,如dotTrace。



假设您使用的是.NET 2.0或更高版本,则可以使用Stopwatch

1
System.Diagnostics.Stopwatch.StartNew();


Stopwatch类还有公共只读字段IsHighResolution,可让您知道秒表是否基于高分辨率性能计数器。如果不是,它基于系统计时器。


我不确定秒表是基于高分辨率性能计数器的。有一些API调用,但我想如果秒表不使用高分辨率,那么API可能不存在。



Stopwatch类的示例

1
2
3
4
5
6
7
8
9
10
    using System.Diagnostics;
    ......
    ...
    ..
    Stopwatch sw = new Stopwatch();
    sw.Start();
    //Your Code Here

    sw.Stop();
    Console.WriteLine("Elapsed={0}",sw.Elapsed);


https://andreyakinshin.gitbooks.io/performancebookdotnet/content/science/microbenchmarking.html


https://github.com/PerfDotNet/BenchmarkDotNet


"实际上,微型标记是非常困难的。如果操作需要10-100ns,操作测量是一个很大的挑战。我建议你使用BenchmarkDotNet作为你的基准测试。它是一个可以帮助你制作一个诚实的基准并获得测量结果的库。精确度很高。当然,你可以在没有任何额外库的情况下编写自己的基准测试。在本节中,我们将讨论为什么它可能是一个坏主意以及在开始之前应该知道什么。"



请参阅Is DateTime的答案。现在是测量函数性能的最佳方法吗?有关高性能测量的解释或阅读我的博客文章


问题是DateTime的分辨率大约是15ms,它不能更精确。然而,秒表可以。



这是MSDN上关于如何为Windows实现连续更新,高分辨率时间提供程序的一篇很好的文章


这是本文的示例源代码(C ++)。



这段代码项目文章展示了如何使用高性能计时器来测量代码的执行速度:


http://www.codeproject.com/KB/cs/highperformancetimercshar.aspx


在这里,您可以找到许多开源C#剖析器:


http://csharp-source.net/open-source/profilers



我喜欢@Evgeniy提到的BenchmarkDotNet。可能他的回复是绕过的,因为没有代码片段,但是因为这是一个复杂的项目,所以至少要先了解一下这个库,然后再先进入定制的东西。


并且因为一些代码总是引人注目,这里是引用网站的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
using System;
using System.Security.Cryptography;
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Running;

namespace MyBenchmarks
{
    [ClrJob(baseline: true), CoreJob, MonoJob, CoreRtJob]
    [RPlotExporter, RankColumn]
    public class Md5VsSha256
    {
        private SHA256 sha256 = SHA256.Create();
        private MD5 md5 = MD5.Create();
        private byte[] data;

        [Params(1000, 10000)]
        public int N;

        [GlobalSetup]
        public void Setup()
        {
            data = new byte[N];
            new Random(42).NextBytes(data);
        }

        [Benchmark]
        public byte[] Sha256() => sha256.ComputeHash(data);

        [Benchmark]
        public byte[] Md5() => md5.ComputeHash(data);
    }

    public class Program
    {
        public static void Main(string[] args)
        {
            var summary = BenchmarkRunner.Run<Md5VsSha256>();
        }
    }
}


另一种选择是使用Fody自动插入定时器代码。这使您的代码更容易阅读,因为它分离了您的跨领域问题。我认为这与所谓的面向方面编程很接近,但是在编译后的时间完成。


有关进行方法计时的fody插件,请参阅https://github.com/Fody/MethodTimer。


引用自述文件:


使用拦截器,在程序集中的某个位置:

1
2
3
4
5
6
public static class MethodTimeLogger {
  public static void Log(MethodBase methodBase, long milliseconds)
  {
    //Do some logging here
  }
}


你的代码,

1
2
3
4
5
6
7
8
9
public class MyClass
{
    [Time]
    public void MyMethod()
    {
        //Some code u are curious how long it takes
        Console.WriteLine("Hello");
    }
}


编译成这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class MyClass
{
    public void MyMethod()
    {
        var stopwatch = Stopwatch.StartNew();
        try
        {
            //Some code u are curious how long it takes
            Console.WriteLine("Hello");
        }
        finally
        {
            stopwatch.Stop();
            MethodTimeLogger.Log(methodof(MyClass.MyMethod), stopwatch.ElapsedMilliseconds);
        }
    }
}


一次性风格Stopwatch最适合我。

1
2
3
4
5
6
7
8
9
10
class VWatch : IDisposable {
    Stopwatch watch = new Stopwatch();
    public VWatch() {
        this.watch.Start();
    }
    public void Dispose() {
        this.watch.Stop();
        Console.WriteLine("Finished. Elapsed={0}", this.watch.Elapsed);
    }
}


然后:

1
2
3
using (new VWatch()) {
    /// do something for time measurement
}


有时最好查看为什么需要计时操作?它运行缓慢吗?或者你只是好奇吗?优化的第一条规则是"不要这样做"。因此,根据您实际测量的内容,可能会改变对哪种工具最适合该任务的意见。



我做了一个扩展,从ticks返回毫秒。

1
2
3
4
5
public static int GetTotalRunningTimeInMilliseconds(this DateTime start)
{
    var endTicks = DateTime.Now.Ticks - start.Ticks;
    return TimeSpan.FromTicks(endTicks).Milliseconds;
}


用法:

1
2
3
4
5
 var start = DateTime.Now;

 //...your long running code here

 var endTime = start.GetTotalRunningTimeInMilliseconds();



我已经做了一个非常简单的方法来测量Action的执行速度,这对我来说有好处,我可以在需要的时候重用它,以及我必须测量的代码。


对我来说,DateTime已经足够了,但它很容易适应从DateTime到秒表。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public static TimeSpan MeasureTime(Action action)
{
    DateTime start = DateTime.Now;

    if (action == null)
    {
        throw new ArgumentNullException("action");
    }

    try
    {
        action();
    }
    catch (Exception ex)
    {
        Debugger.Log(1,"Measuring",ex.ToString());
    }

    return DateTime.Now - start;
}


如何使用它?:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
private static void StressTest()
{
    List<TimeSpan> tss = new List<TimeSpan>();

    for (int i = 0; i < 100; i++)
    {
        // here is the measuring:
        var ts = MeasureTime(() => instance.Method("param1"));

        tss.Add(ts);
    }

    Console.WriteLine("Max: {0}", tss.Max());
    Console.WriteLine("Min: {0}", tss.Min());
    Console.WriteLine("Avg: {0}", TimeSpan.FromMilliseconds(tss.Average(i => i.TotalMilliseconds)));
}


要么:

1
2
3
4
5
6
7
8
9
10
var ts = MeasureTime(() =>
                        {
                            // Some intensive stuff here
                            int a = 1;

                            // more here
                            int b = 2;

                            // and so on
                        });


最容易使用像ANTS Performance Profiler这样的分析器,或者其他可用的分析器。



为了测量测量之间差异的性能,我使用这个类。 StopWatch类没有Split方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
/// <summary>
/// Stopwatch like class that keeps track of timelapses.
/// Probably low-res because of the usage of DateTime.
/// </summary>
public class ChronoMeter
{
    /// <summary>
    /// The name given when the Chronometer was constructed.
    /// </summary>
    public string Name { get; private set; }
    /// <summary>
    /// The moment in time Start was called.
    /// </summary>
    public DateTime Started { get; private set; }

    /// <summary>
    /// All time recordings are added to this list by calling Split and Stop.
    /// </summary>
    public List<ChronoRecord> Records { get; private set; }

    private readonly Stopwatch _stopWatch = new Stopwatch();

    private bool _hasBeenStopped = false;

    /// <summary>
    /// Constrcutor
    /// </summary>
    /// <param name="pName">The name is used in logging</param>
    /// <param name="pLoggingType">The type of logging appriate for the information yielded by this time recording.</param>
    public ChronoMeter(string pName)
    {
        Name = pName;
        Records = new List<ChronoRecord>();
    }

    /// <summary>
    /// Not calling Stop is bad practise. Therefore a little safety net zo the end is still recorderd.
    /// Keep in mind that the garbase collector invokes the destructor, so the moment of time probably doesn't make much sense.
    /// It is more to notify that you should have used Stop for the latest split.
    /// </summary>
    ~ChronoMeter()
    {
        if (!_hasBeenStopped)
        {
            Stop("Destructor safety net");
        }
    }

    /// <summary>
    /// TimeElapsedSinceStart of a ChronoRecord is relative to the moment ChronoMeter was started by calling this function.
    /// </summary>
    public void Start()
    {
        _stopWatch.Start();
        Started = DateTime.Now;
    }

    /// <summary>
    /// Splits the timerecording and add a record of this moment to the list of split records.
    /// </summary>
    /// <param name="pSplitName"></param>
    public void Split(string pSplitName)
    {
        _stopWatch.Stop();
        var created = Started + _stopWatch.Elapsed;
        var previousRecord = Records.LastOrDefault();
        Records.Add(new ChronoRecord(pSplitName, Started, created, previousRecord));
        _stopWatch.Start();
    }

    /// <summary>
    /// Indicates you are done and the records will be written to the log.
    /// </summary>
    public void Stop(string pSplitName)
    {
        Split(pSplitName);
        _stopWatch.Stop();
        _hasBeenStopped = true;
    }

    public class ChronoRecord
    {
        public string Name { get; private set; }
        public TimeSpan TimeElapsedSinceStart { get; private set; }
        public TimeSpan TimeElapsedSincePrevious { get; private set; }
        public DateTime Start { get; private set; }
        public DateTime Created { get; private set; }

        public ChronoRecord(string pName, DateTime pStartDateTime, DateTime pCreated, ChronoRecord pPreviousRecord=null)
        {
            if (pCreated == default(DateTime)) //Ignore DefaultDateTimeComparison
            {
                pCreated = DateTime.Now;
            }
            Created = pCreated;
            Name = pName;
            Start = pStartDateTime;

            TimeElapsedSinceStart = Created - Start;
            if (pPreviousRecord != null)
            {
                TimeElapsedSincePrevious = Created - pPreviousRecord.Created;
            }
            else
            {
                TimeElapsedSincePrevious = TimeElapsedSinceStart;
            }
        }
    }
}