性能测试:使用 System.Diagnostics.Stopwatch 进行精确运行时间测量






4.95/5 (51投票s)
如何使用 .NET Framework 的 Stopwatch 类来高精度地测量和比较算法的运行时间
引言
任何进行性能优化的程序员或多或少都会遇到 `System.Diagnostics` 命名空间下的 `Stopwatch` 类。而大家也可能注意到,在同一台电脑上对同一函数进行的测量,其运行时间可能会有 25%-30% 的差异。本文将介绍如何设计单线程测试程序,以达到 `Stopwatch` 类 0.1%-0.2% 的精度。有了这样的精度,就可以测试和比较算法了。
背景
现代 CPU 拥有多核、大缓存、指令流水线以及许多其他会影响特定测试场景下算法运行时间的因素。白盒测试技术(如附加调试器或性能分析器)会破坏 CPU 的缓存行、流水线等。真实的运行时间被隐藏了,导致一个为这些现代超标量 CPU 优化的算法(因为指令更多)反而会比一个未优化的算法在附加性能分析器时运行得更慢。黑盒测试(运行时间测量),在不附加调试器或性能分析器的情况下,能够发现算法的真实性能,从而完成算法的性能分析。
设置测试场景
最重要的一点是防止 CPU 核心或处理器之间的切换。切换会破坏缓存等,并对测试产生巨大的性能影响。这可以通过设置进程的 `ProcessorAffinity` 掩码来实现。
Process.GetCurrentProcess().ProcessorAffinity = new IntPtr(2); // Use only the second core
为了更专心地使用 CPU 核心,我们必须阻止其他线程占用该 CPU 核心。为此,我们将进程和线程的优先级设置为最高。
Process.GetCurrentProcess().PriorityClass = ProcessPriorityClass.High;
Thread.CurrentThread.Priority = ThreadPriority.Highest;
最后但同样重要的是,我们的测试需要一个预热阶段。在我的系统上,预热 1000-1500 毫秒后,结果会趋于稳定。我们可以使用 Stopwatch 本身来控制预热(此处至少 1200 毫秒)。
stopwatch.Start();
while (stopwatch.ElapsedMilliseconds < 1200)
{
result = TestFunction(seed, count);
}
stopwatch.Stop();
以下是完整的示例代码
using System;
using System.Diagnostics;
using System.Threading;
namespace PreciseMeasure
{
class Program
{
static void Main(string[] args)
{
Stopwatch stopwatch = new Stopwatch();
long seed = Environment.TickCount; // Prevents the JIT Compiler
// from optimizing Fkt calls away
long result = 0;
int count = 100000000;
Console.WriteLine("20 Tests without correct preparation");
Console.WriteLine("Warmup");
for (int repeat = 0; repeat < 20; ++repeat)
{
stopwatch.Reset();
stopwatch.Start();
result ^= TestFunction(seed, count);
stopwatch.Stop();
Console.WriteLine("Ticks: " + stopwatch.ElapsedTicks +
" mS: " +stopwatch.ElapsedMilliseconds);
}
Process.GetCurrentProcess().ProcessorAffinity =
new IntPtr(2); // Uses the second Core or Processor for the Test
Process.GetCurrentProcess().PriorityClass =
ProcessPriorityClass.High; // Prevents "Normal" processes
// from interrupting Threads
Thread.CurrentThread.Priority =
ThreadPriority.Highest; // Prevents "Normal" Threads
// from interrupting this thread
Console.WriteLine();
Console.WriteLine();
Console.WriteLine("20 Tests with correct preparation");
Console.WriteLine("Warmup");
stopwatch.Reset();
stopwatch.Start();
while (stopwatch.ElapsedMilliseconds < 1200) // A Warmup of 1000-1500 mS
// stabilizes the CPU cache and pipeline.
{
result = TestFunction(seed, count); // Warmup
}
stopwatch.Stop();
for (int repeat = 0; repeat < 20; ++repeat)
{
stopwatch.Reset();
stopwatch.Start();
result ^= TestFunction(seed, count);
stopwatch.Stop();
Console.WriteLine("Ticks: " + stopwatch.ElapsedTicks +
" mS: " + stopwatch.ElapsedMilliseconds);
}
Console.WriteLine(result); // prevents optimizations (current compilers are
// too silly to analyze the dataflow that deep, but we never know )
}
public static long TestFunction(long seed, int count)
{
long result = seed;
for (int i = 0; i < count; ++i)
{
result ^= i ^ seed; // Some useless bit operations
}
return result;
}
}
}
结果
未经正确准备
- Ticks: 1580367 mS: 572 <-- 最高值
- Ticks: 1577003 mS: 571
- Ticks: 1576140 mS: 571
- Ticks: 1560964 mS: 565
- Ticks: 1351663 mS: 489
- Ticks: 1248383 mS: 452
- Ticks: 1115361 mS: 404
- Ticks: 1112813 mS: 403
- Ticks: 1113112 mS: 403
- Ticks: 1112012 mS: 402 <-- 最低值
- Ticks: 1330444 mS: 482
- Ticks: 1558493 mS: 564
- Ticks: 1501097 mS: 543
- Ticks: 1517796 mS: 549
- Ticks: 1542712 mS: 558
- Ticks: 1574959 mS: 570
- Ticks: 1483975 mS: 537
- Ticks: 1390578 mS: 503
- Ticks: 1546904 mS: 560
- Ticks: 1349507 mS: 488
运行时间在 402 毫秒到 572 毫秒之间变化。差异为 170 毫秒,即 42%。这些结果是无用的。
经过正确准备
- Ticks: 1110518 mS: 402
- Ticks: 1110540 mS: 402
- Ticks: 1110543 mS: 402
- Ticks: 1110684 mS: 402 <-- 最高值
- Ticks: 1110508 mS: 402
- Ticks: 1110553 mS: 402
- Ticks: 1110600 mS: 402
- Ticks: 1110433 mS: 402 <-- 最低值
- Ticks: 1110509 mS: 402
- Ticks: 1110508 mS: 402
- Ticks: 1110489 mS: 402
- Ticks: 1110568 mS: 402
- Ticks: 1110503 mS: 402
- Ticks: 1110566 mS: 402
- Ticks: 1110625 mS: 402
- Ticks: 1110474 mS: 402
- Ticks: 1110571 mS: 402
- Ticks: 1110448 mS: 402
- Ticks: 1110555 mS: 402
- Ticks: 1110495 mS: 402
这 20 个样本产生了相同的 402 毫秒结果,只能通过 Ticks 数(内部 CPU 性能计数器值)来区分。差异为 251 Ticks,或 0.02%。在我的系统上,`Stopwatch` 的频率为每秒 2760029 Ticks。测试运行之间的差异仅为 0.09 毫秒。这非常好,可以用于测量和比较算法的运行时间。
关注点
有一点非常重要需要牢记。未经准备的最好(最低)值不如经过准备的最差值。CPU 上下文和核心切换对应用程序的运行时间有巨大影响。
链接和附加资源
本文是可以在 benchmarks.codeplex.com 上找到的性能测试之一。这些是为 “Second WAF” Web 应用程序框架 进行的研究。
历史
- 2010年2月28日:初始发布