如何在C#中进行性能优化的单元测试?

29
我正在构建一些搜索代码,并使用优化后的Levenshtein算法。我有功能单元测试来验证算法是否返回正确的结果,但在这种情况下,算法的性能也非常重要。我希望为项目添加一些测试覆盖率,以便如果未来的修改影响了优化,它们将显示为失败的测试 - 因为该算法是确定性的并针对已知的测试数据运行,所以这可以详细到计算给定测试输入的指令执行次数。换句话说,我不是想使用计时器来测量算法性能 - 我感兴趣的是实际测试算法的内部行为而不仅仅是输出。

请问如何在C#/.NET 4中实现此目标?编辑:我不想只使用挂钟时间的原因是它会随着CPU负载和其他无法控制的因素而变化。例如,在构建服务器负载下可能导致测试失败。部署系统将包括挂钟监控。编辑2:这样想...当性能是关键要求时,您将如何应用红-绿-重构?

1
指令执行数量并不一定等于性能。为什么这里不以挂钟时间作为度量标准? - Oliver Charlesworth
2
我不会使用单元测试来验证这个。如果开发人员正在更改工作、完整、高度优化的代码,那么他们应该清楚自己在做什么。这段代码很可能永远不需要被“微调”,对吧?那为什么要费心呢?性能是一种非功能特性。 - Mitch Wheat
1
在单元测试中运行一些计时器。如果它们比应该的时间大一个数量级,则失败。即使在 CPU 负载下,它们仍应通过;如果由于服务器负载而失败,请将它们放在负载较小的服务器上。 - Robert Harvey
1
@RobertHarvey:一个单元测试如果基于外部因素可能随机通过或失败,可以说不是一个合适的单元测试... - Oliver Charlesworth
1
@OliCharlesworth:如果它失败了,你就知道有问题了。 - Robert Harvey
显示剩余2条评论
1个回答

36

我将回答你问题的第三部分,因为我曾经成功地执行过几次。

当性能是关键要求时,如何应用红-绿-重构?

  1. 编写钉住测试来捕捉回归,包括您计划更改和其他可能因您的更改而变慢的方法。
  2. 编写失败的性能测试。
  3. 进行性能改进,经常运行所有测试。
  4. 更新您的固定测试以更紧密地固定性能。

编写钉住测试

创建一个像这样的帮助方法,以测量您想要固定的内容。

private TimeSpan Time(Action toTime)
{
    var timer = Stopwatch.StartNew();
    toTime();
    timer.Stop();
    return timer.Elapsed;
}

然后编写一个测试,断言您的方法不需要任何时间:

[Test]
public void FooPerformance_Pin()
{
    Assert.That(Time(()=>fooer.Foo()), Is.LessThanOrEqualTo(TimeSpan.FromSeconds(0));
}

如果它失败了(故障消息中显示实际经过的时间),请使用略高于实际时间的时间更新时间。重新运行,就可以通过测试了。对于其他可能会受到更改影响的功能,重复这个步骤,最终得到类似下面的结果。

[Test]
public void FooPerformance_Pin()
{
    Assert.That(Time(()=>fooer.Foo()), Is.LessThanOrEqualTo(TimeSpan.FromSeconds(0.8));
}
[Test]
public void BarPerformance_Pin()
{
    Assert.That(Time(()=>fooer.Bar()), Is.LessThanOrEqualTo(TimeSpan.FromSeconds(6));
}

编写一个失败的性能测试

我喜欢把这种测试称为“引诱测试”。它只是针对固定测试的第一步。

[Test]
public void FooPerformance_Bait()
{
    Assert.That(Time(()=>fooer.Foo()), Is.LessThanOrEqualTo(TimeSpan.FromSeconds(0));
}

现在,着手进行性能改进。每次尝试改进后都要运行所有测试(包括锁定和诱饵测试)。如果成功了,你会看到诱饵测试的失败输出中的时间减少了,而你的锁定测试都不会失败。

当你对改进满意时,更新你所更改代码的锁定测试,并删除诱饵测试。

现在你该怎样处理这些测试呢?

最不必担心的做法是将这些测试标记为Explicit属性,并将它们保留到下次检查性能时使用。

另一方面,为这种类型的测试创建一个相当好控制的 CI 子系统,是监测性能回归的一种非常好的方法。根据我的经验,人们实际上更担心它们“由于其他原因导致 CPU 负载随机失败”而不是实际故障。这种努力的成功更多地取决于团队文化而不是你对环境的控制能力。


1
我不确定我理解你的最后一条评论。当然,这与拥有良好控制的 CI 有关,确保在一个特定的机器上运行性能测试,一次只运行一个测试,并尽可能禁用许多后台进程等等。也许在开始运行之前还要确保清除缓存。如果您没有完成以上大部分/全部操作,我不认为您如何进行有意义的性能回归测试(除非您设置了非常宽松的阈值)。 - Oliver Charlesworth
我认为你可能是对的 - 特别是关于需要一个良好控制的CI子系统来监测性能回归的需求。我想在这里使用专用的TeamCity构建代理可能是一个不错的方法 - 我们之前已经为长时间运行的Selenium测试等类似事情做过类似的事情。感谢你如此全面的回答。 - Dylan Beattie
@OliCharlesworth,你有没有尝试在CI中运行这样的性能测试?第一次我这样做时遇到了很多阻力。事实是,虽然设置需要一些工作,但让它们稳定下来并不需要太多的工作。在我们尝试之前,与反对者争论要花费更多的工作。你的情况可能会有所不同,大多数这些测试测量的是几秒钟而不是毫秒级别的东西。非常快速的性能测试将更容易受到外部条件的影响,我想。 - tallseth
1
这个答案中也有一些不错的想法:https://dev59.com/onNA5IYBdhLWcg3wa9Kp#16157458 - asgerhallas

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接