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

易于使用的性能测试组件

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.66/5 (26投票s)

2004年7月20日

CPOL

5分钟阅读

viewsIcon

89004

downloadIcon

1451

一个用于实现 .NET 代码性能测试和计时的组件。

Visual Studio Screenshot

概述

本文介绍了一个计时器组件,该组件可以实现微秒级的计时精度。

引言

软件开发中常常被忽略的任务之一就是性能测试。大多数时候,代码被设计成快速运行,然后经过测试以确保它确实如此。虽然有很多方法可以测试性能并识别代码中的慢速部分,但要精确地定位或计时代码的特定部分通常很困难。使用普通的时间函数,包括计时器滴答函数,最多只能达到毫秒级的精度。此外,你不想为了简单地计时一段代码而复制代码库类或大量代码。因此,我决定编写一个通用计时器,它可以

  1. 能够以微秒级的分辨率计时代码。
  2. 非常容易使用。

背景

为了拥有一个分辨率为微秒级的计时器,我知道有两种可能性:

  1. RDTSC 指令:所有 Pentium 和 Athlon 处理器都可用。
  2. QueryPerformanceCounter:这是一个 Windows API 调用,用于高性能计数器,该计数器通常以远超 1MHz 的速度运行。

虽然第一种选择是最精确和最快的,但我决定使用第二种选择,因为它更具可移植性。QueryPerformanceCounter 在所有 Windows 平台(包括 Pocket PC 设备)上都得到支持。

第二个要求是计时器非常容易使用。对我来说,很明显计时器应该是一个强命名的类,这样它就可以放在 GAC 中。这将使其非常容易包含在程序集中。我还认为,如果它是一个组件,使用起来会更方便。这样,如果在表单(Windows 或 Web)上进行计时,组件就可以从工具箱拖到页面上。

StopWatch 组件中的公共方法

该组件使用起来非常简单,并具有 2 个主要方法:

  1. Reset():此方法将计数重置为 0,可以随时调用。
  2. Trace():此方法有 2 个重载。一个不接受参数,仅在调试输出窗口中显示已用时间。另一个接受一个字符串作为参数,在显示时间之前显示该字符串。根据已用时间,时间以微秒 (us)、毫秒 (ms) 或秒 (s) 显示。

实现

代码依赖于两个 API 调用:

  • QueryPerformanceFrequency
  • QueryPerformanceCounter

这些在 Windows 平台上的 KERNEL.DLL 中定义,在 Pocket PC 上的 CoreDll.dll 中定义,并通过 P/Invoke 调用,如下所示:

#if NET_CF
    [System.Runtime.InteropServices.DllImport("CoreDll.dll")]
#else
    [System.Runtime.InteropServices.DllImport("Kernel32.dll")]
#endif
private static extern int QueryPerformanceFrequency(ref Int64 lpFrequency);

#if NET_CF
    [System.Runtime.InteropServices.DllImport("CoreDll.dll")]
#else
    [System.Runtime.InteropServices.DllImport("Kernel32.dll")]
#endif
private static extern int QueryPerformanceCounter(ref Int64 lpPerformanceCount);

如果库将在 Pocket PC 上使用,则应在生成设置中定义 NET_CF

该组件包含一个名为 Time_us 的属性,该属性是只读的,并以如下方式返回已用时间:

public double Timer_us
{
    get
    {
        QueryPerformanceCounter(ref m_LastCount);
        Int64 Count = m_LastCount;
        Count -= m_TimerStartCount;
        return (double)Count / (double)m_TimerFreq * 1000000.0;
    }
}

ResetTrace 方法实现如下:

public void Reset()
{
    QueryPerformanceFrequency(ref m_TimerFreq);
    QueryPerformanceCounter(ref m_TimerStartCount);
}

public void Trace(string msg)
{
    double t1 = Timer_us;
    Int64 c1 = m_LastCount;
    StringBuilder s1 = new StringBuilder();
    if (t1 < 1000)
        s1.AppendFormat("{0} Time = {1} us", msg, t1.ToString("F2"));
    else if (t1 < 1000000)
        s1.AppendFormat("{0} Time = {1} ms", msg, (t1/1000).ToString("F2"));
    else
        s1.AppendFormat("{0} Time = {1} s", msg, (t1/1000000).ToString("F2"));
    System.Diagnostics.Trace.WriteLine(s1);
    //...trace compensation needed here!

}

Trace.WriteLine 语句执行需要相当长的时间(毫秒)。目前的此代码将严重影响显示的计时。为了尝试补偿这一点,我决定在 Trace 语句执行后获取时间,并将其从开始时间中减去。这意味着,如果您连续调用 Trace,您将得到的时间差异约为 1 或 2 us,而不是毫秒级的差异。虽然这意味着显示的计时不是已用时间的真实指示,但我认为这种行为更有用。不幸的是,我无法可靠地补偿 QueryPerformanceCounter 调用,因此连续调用 Trace 导致我的 PC 上计时时间增加了约 1.4us。其中大部分时间是因为 P/Invoke 开销。代码如下:

    double t2 = Timer_us;
    Int64 c2 = m_LastCount;
    m_TimerStartCount += (c2-c1); //Take account of the trace statement

实现的最后一部分是组件的部署。我编写了一个批处理文件(作为生成过程的一部分)将程序集复制到 Visual Studio 目录并将其安装在全局程序集缓存中。这样,就可以轻松地将其添加到“引用”和“组件工具箱”中。(在“添加引用”对话框中,查找“Nethercott.Timing”。在工具箱的“添加/删除项”对话框中,查找“StopWatch”。)显然,组件只需在工具箱中添加一次(例如,在“组件”选项卡下),即可用于多个解决方案。

使用组件

该组件使用起来非常方便。有两种包含它的方法。最简单的方法是将组件从工具箱拖到 Windows 或 Web 窗体上。然后可以在后台代码页面中按需调用 Reset()Trace() 方法。使用该组件的另一种方法是手动包含该组件。这意味着添加对该类的引用,构造 StopWatch 对象,然后按需调用 Reset()Trace() 方法。虽然不是绝对必要,但在不再需要该对象时调用 Dispose() 可能是个好主意。

参考文献

CodeProject 上还有一些关于计时和计时器类的文章。其中一些是:

历史

  • 2004 年 7 月 19 日 - 初始版本。
© . All rights reserved.