65.9K
CodeProject 正在变化。 阅读更多。
Home

执行计时器

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.62/5 (14投票s)

2004年11月15日

7分钟阅读

viewsIcon

80708

downloadIcon

995

一组用于测量代码执行时间的类。

引言

大多数程序员时不时地会进行某种粗略的性能测试。其中最常见的一种可能是比较两个或多个方法的执行时间,以确定哪个方法效率最高。

虽然实现这类比较测试并不难(有时它们的代码行数不超过 5-6 行,包括将结果写入控制台),但我认为编写一个小型库会是个好主意,这不仅可以使许多人已经做的事情更快更容易地复制,还可以实现一些稍微更复杂的场景。

此库中的类

  • ExectuionTimer - 这是主类。它包含计时方法执行和显示结果的函数。
  • ExecutionTimerCollection - 这是 ExecutionTimer 对象的集合。
  • ExecutionTimerManager - 此类用于管理一组 ExecutionTimer
  • BlockTimer - 此类实现了 IDisposable 接口,用于 C# 'using' 语句。
  • TimedMethod - 此类保存对一个或多个委托的引用,这些委托的目标方法将被计时。
  • TimedMethodCollection - TimedMethod 对象的集合。
  • NameExistsExcpetion - 当一个 ExecutionTimer 被添加到 ExecutionTimerManager 中,而该管理器已包含同名的计时器时发生。
  • SeriesAbortedException - 当 TimerManagerException 无法运行一个系列时发生。

本文不会涵盖所有内容;我将尽量只介绍要点。

BlockTimer 类

测量方法执行时间最直接的方法是简单地记录被计时方法执行前后的系统滴答计数。

// This is the method we'll be timing throughout the article
void TakeOneSecond()
{
  System.Threading.Thread.Sleep(1000);
}
 
long start = DateTime.Now.Ticks;
MethodToTime();
long end = DateTime.Now.Ticks;
 
long executionTime = end - start;
 
Console.WriteLine("MethodToTime took {0} ticks to execute.", 
                                             executionTime);

现在,这当然不难,但可以变得更容易。ExecutionTimer(稍后介绍)的静态 TimeBlock 返回一个实现 IDisposableBlockTimer 对象。这是 BlockTimer 类的完整代码:

public class BlockTimer : IDisposable
{
   private string _description;
   private long    _start;
 
   public BlockTimer(string description)
   {
      _description = description;
      _start = DateTime.Now.Ticks;
   }
 
   public void Dispose()
   {
      long totalTime = DateTime.Now.Ticks - _start;
      Console.WriteLine(_description);
      Console.Write(" - Total Execution Time: ");
      Console.Write(new TimeSpan(totalTime).TotalMilliseconds.ToString());
      Console.WriteLine(" ms.");
   }
}

这个小类允许将最后一个示例重写为这样:

using(ExecutionTimer.TimeBlock("MethodToTime"))
{
   TakeOneSecond();
}
 
// Output:
// Timer for TakeOneSecond
//  - Total Execution Time: 1000 ms.

注意:BlockTimer 的输出单位是毫秒,因为我认为这比滴答更有用。

如果您只需要用一行代码和几个花括号替换 3-4 行代码,那么您就完成了。但是,如果您想要更多地控制输出和/或如何计时,还有更多内容。

ExecutionTimer 类

此库中的主类是 ExecutionTimer 类。此类最简单的用法如下(此代码实际上并不会比使用 BlockTimer 获得更多好处):

ExecutionTimer timer = new ExecutionTimer("TakeOneSecond");
timer.Start();

// run the method being timed
TakeOneSecond();

timer.Stop();

// Write the results
// Note: Setting ExecutionTimer.AutoTrace to true
// would cause this to be called automatically when
// the timer is stopped.
Timer.Write();

// Output:
// ***********************
// Execution time for "TakeOneSecond" (Run 1): 1000 ms.

在运行任何性能测试以确保更准确结果的一种方法是多次运行它。ExecutionTimer 类通过在多次调用 StartStop 时将先前运行添加到系列来支持这一点。这是一个运行系列示例:

// We'll pass true as the second parameter so each run will
// be written to the Console automatically
ExecutionTimer timer = new ExecutionTimer("TakeOneSecond", true);

// We'll do three runs
for(int i = 0; i < 3; i++)
{
   timer.Start();
   TakeOneSecond();
   timer.Stop();
}

// Write the statistics for the series
timer.WriteSeries();

// Output:
// ***********************
// Execution time for "TakeOneSecond" (Run 1): 1000 ms.
//
// ***********************
// Execution time for "TakeOneSecond" (Run 2): 1000 ms.
//
// ***********************
// Execution time for "TakeOneSecond" (Run 3): 1000 ms.
//
// ***********************
// Stats for "TakeOneSecond":
// - Number of runs completed: 3
// - Average execution time per run: 1000 ms.
// - Shortest run: 1000 ms.
// - Longest run: 1000 ms.

现在,您不仅可以获得每次单独运行的结果(如果 AutoTrace 设置为 true),还可以获得运行总数、平均执行时间以及最短和最长运行时间。

结果输出方式有几种不同的方法。ExecutionTimer 类具有 BeginDelimEndDelim 属性,您可以使用它们来指定在每次运行结果之前和之后打印的内容,以及 SeriesBeginDelimSeriesEndDelim,它们对系列结果执行相同的操作。正如您从前面的示例中看到的,BeginDelimSeriesBeginDelim 的默认字符串是“***********************”,而 EndDelimSeriesEndDelim 的默认字符串是空字符串。

上面提到的每个属性还有一个“伴随”属性,它是一个 PrintString 类型的委托,如果定义了,它将用于代替分隔字符串。伴随属性的名称(不出所料)是 BeginPrintStringEndPrintStringSeriesBeginPrintStringSeriesEndPrintString。这是 PrintString 委托的定义:

public delegate string PrintString(string timerName);

传递给 PrintString 委托的参数是相关计时器的名称。下面是一个如何使用 *Delim*PrintString 属性的示例:

private string GetExecutionDate(string timerName)
{
   string header = timerName + " is being timed on ";
   header += DateTime.Now.ToShortDateString();
   header += " at ";
   header += DateTime.Now.ToLongTimeString();
   return header; 
}

ExecutionTimer timer = new ExecutionTimer("Example", true);

// Set the method to be called before each run is printed
timer.BeginPrintString = new PrintString(GetExecutionDate);

// Set the string to be written before the series results are written
timer.SeriesBeginDelim = "Running series with blah, blah set to something";
timer.Start();
TakeOneSecond();
timer.Stop();

timer.WriteSeries();

为了讨论 ExecutionTimer 类的一些其他功能,我需要先介绍另一个类。即 TimedMethod 类。

TimedMethod 类

TimedMethod 类用于方便地使用委托来表示要计时的函数。TimedMethodMethod 属性表示由 ExecutionTimer 计时的主要函数。为了澄清我的意思,这里有一个小的示例,展示了 ExecutionTimer 的一个我已经展示过的用法,以及 TimedMethod 的等效用法:

ExecutionTimer timer = new ExecutionTimer("TakeOneSecond", true);

for(int i = 0; i < 3; i++)
{
   timer.Start();
   TakeOneSecond();
   timer.Stop();
}

timer.WriteSeries();

// Now do the same thing with delegates
ExecutionTimer timerWithDelegates = new ExecutionTimer("With Delegates", true");

// We'll pass the
timerWithDelegates.Method = new TimedMethod(new MethodInvoker(TakeOneSecond), 3);

timerWithDelegates.RunTimedMethod();

正如您可能从上面的示例中推断出的,ExecutionTimerRunTimedMethod 方法会运行指定委托指向的函数,并执行指定次数的迭代,在每次运行之前和之后调用 StartStop

我在这里使用了 MethodInvoker 委托,它定义在 System.Windows.Forms 命名空间中,但 TimedMethodMethod 属性是基类型 Delegate,因此您可以使用任何合适的委托。如果您的委托需要参数,可以通过 TimedMethodArgs 属性传递。

我们到目前为止一直在计时的函数(TakeOneSecond)不改变任何对象的状态,并且可以连续多次调用并产生相同的结果。但是,如果被计时的函数会改变需要在函数再次运行时重置的内容,情况就不同了。为此,TimedMethod 类定义了另外两个委托属性:SetUpTearDown,它们分别在被计时函数之前和之后调用。这两个属性也有伴随属性(SetUpArgsTearDownArgs),用于将参数传递给这些函数(如果有)。

回到 ExecutionTimer 类

到目前为止,所有结果都已写入控制台。但是,ExecutionTimer 类有一个 Out 属性,其类型为 TextWriter。这可以用于将输出重定向到任何最合适的位置,例如字符串以显示在消息框中,或文本文件,或解析后输入到数据库。

ExecutionTimer timer = new ExecutionTimer("Example", true);

// Instead of writing to the console, we'll write to a file
StreamWriter writer = new StreamWriter("Execution Results.log");

timer.Out = writer;

timer.TimedMethod = new TimedMethod("Method 1", 
                    new MethodInvoker(TakeOnSecond), 100));

timer.RunTimedMethod();

writer.Close();

ExecutionTimer 类的事件

ExecutionTimer 类定义了六个事件:TimerStartingTimerStopped(没有相应的 TimerStartedTimerStopping 事件,因为它们会改变记录的执行时间)、ResultsPrintingResultsPrintedSeriesResultsPrintingSeriesResultsPrinted

它们可用于根据即将打印的内容、最近一次运行的执行时间或其他您认为合适的内容,来更改 ExecutionTimer 的属性,例如结果的打印位置、分隔符字符串等。

ExecutionTimerManager 类

ExecutionTimerManager 类主要为了方便而存在。它允许您用更少的代码行来控制一组 ExecutionTimer 对象。这对于设置自动运行的测试很有用。我可以想象使用配置文件来指定要运行的测试以及运行它们的选项,但我还没有实现任何类似的功能。

下面是使用 ExecutionTimerManager 类的一个示例:

void RunManager()
{
   TimedMethodCollection methods = new TimedMethodCollection();
 
   methods.Add(new TimedMethod("No args", 
               new MethodInvoker(MethodToTime), 100)); 
   methods.Add(new TimedMethod("Int arg", 
               new VoidIntArg(MethodToTime), new object[]{10}, 100));
   methods.Add(new TimedMethod("String arg", 
               new VoidStringArg(MethodToTime), new object[]{" "}, 100));
        
   ExecutionTimerManager manager = new ExecutionTimerManager();
            
   StreamWriter writer = new StreamWriter("Managed Timers.log");
   manager.Out = writer;
 
   manager.ExecuteTimers(methods,false);
 
   manager.WriteAll();
 
   writer.Close();                
}
 
delegate void VoidIntArg(int iterations);
delegate void VoidStringArg(string arg);
 
private string GetResultsHeader(string timerName)
{
   string result = "Execution time for ";
   result += timerName;
   result += ":";
   return result;
}
 
private void MethodToTime()
{
   string res = "";
   for(int i = 0; i < 10000; i++)
   {
      res += " ";
   }
}
 
private void MethodToTime(int iterations)
{
   string res = "";
   for(int i = 0; i < iterations; i++)
   {
      res += " ";
   }
}
 
private void MethodToTime(string str)
{
   for(int i = 0; i < 10000; i++)
   {
      str += " ";
   }
}

结论

我想明确说明,这不是一个商业级性能测试工具,或者类似的工具。在进行性能测试时,有很多变数需要考虑,而我并没有考虑……嗯,任何一个。此外,您调用的任何函数的第一次运行通常是最长的,因为它直到您第一次调用它时才会被 JIT 编译。

这些类总体上能够很好地告诉您哪个例程最快,但它显然不会提出任何改进建议,因为它不是代码分析工具。如果您了解它的用途,您可能会发现它很有帮助。

我还想补充一点,这个库的源代码经过了详细注释。本文中的一些代码示例直接摘自代码注释。我使用了 C# 编译器的 XML 注释功能。如果您有 NDoc,您可以使用它来为文档构建 MSDN 风格的网页。如果您没有 NDoc,我**强烈**推荐它。一旦我开始考虑使用 NDoc 构建最终文档时,我的注释质量就得到了显著提高。您可以在这里找到它。

    © . All rights reserved.