易于使用的性能测试组件






4.66/5 (26投票s)
一个用于实现 .NET 代码性能测试和计时的组件。
概述
本文介绍了一个计时器组件,该组件可以实现微秒级的计时精度。
引言
软件开发中常常被忽略的任务之一就是性能测试。大多数时候,代码被设计成快速运行,然后经过测试以确保它确实如此。虽然有很多方法可以测试性能并识别代码中的慢速部分,但要精确地定位或计时代码的特定部分通常很困难。使用普通的时间函数,包括计时器滴答函数,最多只能达到毫秒级的精度。此外,你不想为了简单地计时一段代码而复制代码库类或大量代码。因此,我决定编写一个通用计时器,它可以
- 能够以微秒级的分辨率计时代码。
- 非常容易使用。
背景
为了拥有一个分辨率为微秒级的计时器,我知道有两种可能性:
RDTSC
指令:所有 Pentium 和 Athlon 处理器都可用。QueryPerformanceCounter
:这是一个 Windows API 调用,用于高性能计数器,该计数器通常以远超 1MHz 的速度运行。
虽然第一种选择是最精确和最快的,但我决定使用第二种选择,因为它更具可移植性。QueryPerformanceCounter
在所有 Windows 平台(包括 Pocket PC 设备)上都得到支持。
第二个要求是计时器非常容易使用。对我来说,很明显计时器应该是一个强命名的类,这样它就可以放在 GAC 中。这将使其非常容易包含在程序集中。我还认为,如果它是一个组件,使用起来会更方便。这样,如果在表单(Windows 或 Web)上进行计时,组件就可以从工具箱拖到页面上。
StopWatch 组件中的公共方法
该组件使用起来非常简单,并具有 2 个主要方法:
Reset()
:此方法将计数重置为 0,可以随时调用。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;
}
}
Reset
和 Trace
方法实现如下:
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 上还有一些关于计时和计时器类的文章。其中一些是:
- 高级单元测试,第四部分 - Fixture 设置/拆卸、测试重复和性能测试
这是一篇非常详细的文章,介绍了如何在单元测试中包含性能度量。它还展示了如何限制 JIT 编译、垃圾回收和任务切换的误导性影响,以使结果更可靠。
- 用两种简单的方法获取处理器速度
这是使用
QueryPerformanceCounter
和RDTSC
来计算处理器速度。 - C# 中的高性能计时器
- HighResClock - C# 高分辨率时钟类
通用计时类(使用
QueryPerformanceCounter
)。 - CPerfTimer 计时器类 (C++)
C++ 的通用计时器类。
历史
- 2004 年 7 月 19 日 - 初始版本。