性能测试的精确时间测量

怎样才能最准确地看到某个东西(例如一个方法调用)在代码中花费了多长时间?

我猜最简单和最快的方法是:

DateTime start = DateTime.Now;
{
// Do some work
}
TimeSpan timeItTook = DateTime.Now - start;

但这有多精确呢?有没有更好的办法?

289500 次浏览

使用秒表

更好的方法是使用Stopwatch类:

using System.Diagnostics;
// ...


Stopwatch sw = new Stopwatch();


sw.Start();


// ...


sw.Stop();


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

stopwatch就是为这个任务设计的。

正如其他人所说,Stopwatch是这里使用的一个很好的类。你可以用一个有用的方法来包装它:

public static TimeSpan Time(Action action)
{
Stopwatch stopwatch = Stopwatch.StartNew();
action();
stopwatch.Stop();
return stopwatch.Elapsed;
}

(注意Stopwatch.StartNew()的使用。我更喜欢这样创建一个秒表,然后调用Start()简单。)显然,这会引起调用委托的冲击,但在绝大多数情况下,这是无关紧要的。你可以这样写:

TimeSpan time = StopwatchUtil.Time(() =>
{
// Do some work
});

你甚至可以为此创建一个ITimer接口,在可用的地方使用StopwatchTimer, CpuTimer等实现。

秒表很好,但循环工作10^6次,然后除以10^6。 你会得到更高的精度。

我用这个:

HttpWebRequest request = (HttpWebRequest)WebRequest.Create(myUrl);
System.Diagnostics.Stopwatch timer = new Stopwatch();


timer.Start();


HttpWebResponse response = (HttpWebResponse)request.GetResponse();


statusCode = response.StatusCode.ToString();


response.Close();


timer.Stop();

正如其他人所说,Stopwatch应该是正确的工具。不过,可以对它进行一些改进,具体参见这个线程:在c#中测试小代码样本,这个实现可以改进吗?

我已经看到了我是Thomas Maierhofer的一些有用的技巧

他的代码大致如下:

//prevent the JIT Compiler from optimizing Fkt calls away
long seed = Environment.TickCount;


//use the second Core/Processor for the test
Process.GetCurrentProcess().ProcessorAffinity = new IntPtr(2);


//prevent "Normal" Processes from interrupting Threads
Process.GetCurrentProcess().PriorityClass = ProcessPriorityClass.High;


//prevent "Normal" Threads from interrupting this thread
Thread.CurrentThread.Priority = ThreadPriority.Highest;


//warm up
method();


var stopwatch = new Stopwatch()
for (int i = 0; i < repetitions; i++)
{
stopwatch.Reset();
stopwatch.Start();
for (int j = 0; j < iterations; j++)
method();
stopwatch.Stop();
print stopwatch.Elapsed.TotalMilliseconds;
}

另一种方法是依赖Process.TotalProcessTime来测量CPU一直处于忙碌状态的时间。这可以反映更真实的场景,因为没有其他进程影响测量。它是这样做的:

 var start = Process.GetCurrentProcess().TotalProcessorTime;
method();
var stop = Process.GetCurrentProcess().TotalProcessorTime;
print (end - begin).TotalMilliseconds;

在这里也可以找到同样的东西。的一个详细的裸实现

我写了一个助手类,以一种易于使用的方式来执行这两项:

public class Clock
{
interface IStopwatch
{
bool IsRunning { get; }
TimeSpan Elapsed { get; }


void Start();
void Stop();
void Reset();
}






class TimeWatch : IStopwatch
{
Stopwatch stopwatch = new Stopwatch();


public TimeSpan Elapsed
{
get { return stopwatch.Elapsed; }
}


public bool IsRunning
{
get { return stopwatch.IsRunning; }
}






public TimeWatch()
{
if (!Stopwatch.IsHighResolution)
throw new NotSupportedException("Your hardware doesn't support high resolution counter");


//prevent the JIT Compiler from optimizing Fkt calls away
long seed = Environment.TickCount;


//use the second Core/Processor for the test
Process.GetCurrentProcess().ProcessorAffinity = new IntPtr(2);


//prevent "Normal" Processes from interrupting Threads
Process.GetCurrentProcess().PriorityClass = ProcessPriorityClass.High;


//prevent "Normal" Threads from interrupting this thread
Thread.CurrentThread.Priority = ThreadPriority.Highest;
}






public void Start()
{
stopwatch.Start();
}


public void Stop()
{
stopwatch.Stop();
}


public void Reset()
{
stopwatch.Reset();
}
}






class CpuWatch : IStopwatch
{
TimeSpan startTime;
TimeSpan endTime;
bool isRunning;






public TimeSpan Elapsed
{
get
{
if (IsRunning)
throw new NotImplementedException("Getting elapsed span while watch is running is not implemented");


return endTime - startTime;
}
}


public bool IsRunning
{
get { return isRunning; }
}






public void Start()
{
startTime = Process.GetCurrentProcess().TotalProcessorTime;
isRunning = true;
}


public void Stop()
{
endTime = Process.GetCurrentProcess().TotalProcessorTime;
isRunning = false;
}


public void Reset()
{
startTime = TimeSpan.Zero;
endTime = TimeSpan.Zero;
}
}






public static void BenchmarkTime(Action action, int iterations = 10000)
{
Benchmark<TimeWatch>(action, iterations);
}


static void Benchmark<T>(Action action, int iterations) where T : IStopwatch, new()
{
//clean Garbage
GC.Collect();


//wait for the finalizer queue to empty
GC.WaitForPendingFinalizers();


//clean Garbage
GC.Collect();


//warm up
action();


var stopwatch = new T();
var timings = new double[5];
for (int i = 0; i < timings.Length; i++)
{
stopwatch.Reset();
stopwatch.Start();
for (int j = 0; j < iterations; j++)
action();
stopwatch.Stop();
timings[i] = stopwatch.Elapsed.TotalMilliseconds;
print timings[i];
}
print "normalized mean: " + timings.NormalizedMean().ToString();
}


public static void BenchmarkCpu(Action action, int iterations = 10000)
{
Benchmark<CpuWatch>(action, iterations);
}
}

就叫

Clock.BenchmarkTime(() =>
{
//code


}, 10000000);

Clock.BenchmarkCpu(() =>
{
//code


}, 10000000);

Clock的最后一部分是比较棘手的部分。如果你想显示最终的计时,由你来选择你想要的计时类型。我写了一个扩展方法NormalizedMean,它给你读取时间的平均值NormalizedMean0我的意思是我计算每个时间与实际平均值的偏差,然后我放弃了离NormalizedMean1更远的值(只有较慢的值)(称为绝对偏差;注意,它不是经常听到的标准偏差),最后返回剩余值的平均值。这意味着,例如,如果计时值是{ 1, 2, 3, 2, 100 }(毫秒或其他单位),它会丢弃100,并返回{ 1, 2, 3, 2 }的平均值,即2。或者如果计时为{ 240, 220, 200, 220, 220, 270 },则丢弃270,并返回{ 240, 220, 200, 220, 220 }的平均值,即220

public static double NormalizedMean(this ICollection<double> values)
{
if (values.Count == 0)
return double.NaN;


var deviations = values.Deviations().ToArray();
var meanDeviation = deviations.Sum(t => Math.Abs(t.Item2)) / values.Count;
return deviations.Where(t => t.Item2 > 0 || Math.Abs(t.Item2) <= meanDeviation).Average(t => t.Item1);
}


public static IEnumerable<Tuple<double, double>> Deviations(this ICollection<double> values)
{
if (values.Count == 0)
yield break;


var avg = values.Average();
foreach (var d in values)
yield return Tuple.Create(d, avg - d);
}