【发布时间】:2011-04-29 10:23:39
【问题描述】:
抱歉,这篇文章很长,但我只是在分析本文时解释我的思路。问题在最后。
我了解测量代码运行时间的内容。它运行多次以获得平均运行时间,以解决每次运行的差异,并获得更好地利用缓存的时间。
为了测量某人的运行时间,我在多次修改后想出了this 代码。
最后我得到了这段代码,它产生了我想要捕获的结果,而没有给出误导性的数字:
// implementation C
static void Test<T>(string testName, Func<T> test, int iterations = 1000000)
{
Console.WriteLine(testName);
Console.WriteLine("Iterations: {0}", iterations);
var results = Enumerable.Repeat(0, iterations).Select(i => new System.Diagnostics.Stopwatch()).ToList();
var timer = System.Diagnostics.Stopwatch.StartNew();
for (int i = 0; i < results.Count; i++)
{
results[i].Start();
test();
results[i].Stop();
}
timer.Stop();
Console.WriteLine("Time(ms): {0,3}/{1,10}/{2,8} ({3,10})", results.Min(t => t.ElapsedMilliseconds), results.Average(t => t.ElapsedMilliseconds), results.Max(t => t.ElapsedMilliseconds), timer.ElapsedMilliseconds);
Console.WriteLine("Ticks: {0,3}/{1,10}/{2,8} ({3,10})", results.Min(t => t.ElapsedTicks), results.Average(t => t.ElapsedTicks), results.Max(t => t.ElapsedTicks), timer.ElapsedTicks);
Console.WriteLine();
}
在我见过的所有衡量运行时间的代码中,它们通常采用以下形式:
// 接近 1 伪代码 启动计时器; 循环N次: 运行测试代码(直接或通过函数); 停止计时器; 报告结果;我认为这很好,因为有了这些数字,我就有了总运行时间,并且可以轻松计算出平均运行时间,并且具有良好的缓存局部性。
但我认为重要的一组值是最小和最大迭代运行时间。这无法使用上述表格进行计算。所以当我写我的测试代码时,我是这样写的:
// 方法 2 伪代码 循环N次: 启动计时器; 运行测试代码(直接或通过函数); 停止计时器; 存储结果; 报告结果;这很好,因为我可以找到我感兴趣的最小、最大和平均时间。直到现在我意识到这可能会导致结果出现偏差,因为缓存可能会受到影响,因为循环没有t 非常紧,给我的结果不是最佳。
我编写测试代码的方式(使用 LINQ)增加了我知道但忽略的额外开销,因为我只是测量正在运行的代码,而不是开销。这是我的第一个版本:
// implementation A
static void Test<T>(string testName, Func<T> test, int iterations = 1000000)
{
Console.WriteLine(testName);
var results = Enumerable.Repeat(0, iterations).Select(i =>
{
var timer = System.Diagnostics.Stopwatch.StartNew();
test();
timer.Stop();
return timer;
}).ToList();
Console.WriteLine("Time(ms): {0,3}/{1,10}/{2,8}", results.Min(t => t.ElapsedMilliseconds), results.Average(t => t.ElapsedMilliseconds), results.Max(t => t.ElapsedMilliseconds));
Console.WriteLine("Ticks: {0,3}/{1,10}/{2,8}", results.Min(t => t.ElapsedTicks), results.Average(t => t.ElapsedTicks), results.Max(t => t.ElapsedTicks));
Console.WriteLine();
}
在这里我认为这很好,因为我只是在测量运行测试功能所花费的时间。与 LINQ 相关的开销不包括在运行时间中。为了减少在循环中创建计时器对象的开销,我进行了修改。
// implementation B
static void Test<T>(string testName, Func<T> test, int iterations = 1000000)
{
Console.WriteLine(testName);
Console.WriteLine("Iterations: {0}", iterations);
var results = Enumerable.Repeat(0, iterations).Select(i => new System.Diagnostics.Stopwatch()).ToList();
results.ForEach(t =>
{
t.Start();
test();
t.Stop();
});
Console.WriteLine("Time(ms): {0,3}/{1,10}/{2,8} ({3,10})", results.Min(t => t.ElapsedMilliseconds), results.Average(t => t.ElapsedMilliseconds), results.Max(t => t.ElapsedMilliseconds), results.Sum(t => t.ElapsedMilliseconds));
Console.WriteLine("Ticks: {0,3}/{1,10}/{2,8} ({3,10})", results.Min(t => t.ElapsedTicks), results.Average(t => t.ElapsedTicks), results.Max(t => t.ElapsedTicks), results.Sum(t => t.ElapsedTicks));
Console.WriteLine();
}
这改善了总体时间,但造成了一个小问题。我通过添加每次迭代的时间在报告中添加了总运行时间,但由于时间很短并且没有反映实际运行时间(通常要长得多),因此给出了误导性的数字。我现在需要测量整个循环的时间,所以我离开了 LINQ,最终得到了我现在在顶部的代码。这种混合获得了我认为重要的时间,并且开销最小 AFAIK。 (启动和停止计时器只是查询高分辨率计时器)此外,发生的任何上下文切换对我来说都不重要,因为它是正常执行的一部分。
在某一时刻,我强制线程在循环内让步,以确保它在方便的时间的某个时刻有机会(如果测试代码受 CPU 限制并且根本不会阻塞)。我不太担心正在运行的进程可能会使缓存变得更糟,因为无论如何我都会单独运行这些测试。但是,我得出的结论是,对于这种特殊情况,没有必要这样做。虽然如果它总体上证明是有益的,我可能会将它合并到最终的最终版本中。也许作为某些代码的替代算法。
现在我的问题:
- 我做出了正确的选择吗?一些错误的?
- 我是否在思考过程中对目标做出了错误的假设?
- 最小或最大运行时间是否真的是有用的信息,或者它是一个失败的原因?
- 如果是这样,一般来说哪种方法会更好?循环运行的时间(方法 1)?还是只运行相关代码的时间(方法 2)?
- 我的混合方法一般可以使用吗?
- 应该我屈服(原因在上一段中解释)还是对时代的伤害比必要的更大?
- 有没有我没有提到的更优选的方法?
为了清楚起见,我不是在寻找一个通用的、在任何地方使用的、准确的计时器。我只是想知道一个算法,当我想要一个快速实施、相当准确的计时器来测量代码时,我应该使用一种算法,而库或其他 3rd 方工具不可用。
如果没有异议,我倾向于以这种形式编写我的所有测试代码:
// final implementation
static void Test<T>(string testName, Func<T> test, int iterations = 1000000)
{
// print header
var results = Enumerable.Repeat(0, iterations).Select(i => new System.Diagnostics.Stopwatch()).ToList();
for (int i = 0; i < 100; i++) // warm up the cache
{
test();
}
var timer = System.Diagnostics.Stopwatch.StartNew(); // time whole process
for (int i = 0; i < results.Count; i++)
{
results[i].Start(); // time individual process
test();
results[i].Stop();
}
timer.Stop();
// report results
}
对于赏金,理想情况下,我希望回答上述所有问题。我希望有一个很好的解释,说明我影响代码的想法是否合理(如果不是最理想的,可能还有关于如何改进它的想法),或者如果我的观点有误,请解释为什么它是错误的和/或不必要的,如果适用,提供更好的选择。
总结重要问题和我对做出决定的想法:
-
获得每个单独迭代的运行时间通常是一件好事吗?
通过每个单独迭代的时间,我可以计算其他统计信息,例如最小和最大运行时间以及标准差。所以我可以看看是否有缓存或其他未知因素等因素可能会影响结果。这导致了我的“混合”版本。 -
在实际计时开始之前进行小循环运行也不错吗?
从我对 Sam Saffron's 的回复中想到循环,这是为了增加不断访问内存的可能性将被缓存。这样我只测量所有内容都被缓存的时间,而不是一些没有缓存内存访问的情况。 -
循环中的强制
Thread.Yield()是否会帮助或损害受 CPU 限制的测试用例的时间安排?
如果进程受 CPU 限制,操作系统调度程序会降低此任务的优先级由于 CPU 上的时间不足,可能会增加时间。如果它不受 CPU 限制,我会省略屈服。
根据此处的答案,我将使用最终实现来编写我的测试函数,而无需针对一般情况单独计时。如果我想要其他统计数据,我会将其重新引入测试功能,并应用此处提到的其他内容。
【问题讨论】:
标签: c# benchmarking