当扩展方法存在但未被调用时,代码执行的差异。

12

TL;DR, 问题:

.NET中扩展方法的存在会对代码的执行产生什么影响(例如JIT/优化)?

背景

我在MSTest中遇到了一个测试失败,这取决于是否还测试了另一个似乎无关的程序集。

我注意到了测试失败,并偶然发现如果加载了另一个测试程序集,则只会发生失败。在4.5 CLR下,运行Unittests和Integration test程序集的mstest将开始执行集成测试,并在第21个集成测试下失败,而在4.0 CLR下不会发生这种情况(除配置外相同)。 我从集成测试程序集中删除了除失败的测试之外的所有测试。现在,当两个测试程序集都加载时,mstest会加载两个程序集,然后执行集成测试程序集中的单个测试,该测试会失败。

> mstest.exe /testcontainer:Unittests.dll /testcontainer:IntegrationTests.dll

Loading C:\Proj\Tests\bin\x86\Release\Unittests.dll...
Loading C:\Proj\Tests\bin\x86\Release\Integrationtests.dll...
Starting execution...

Results               Top Level Tests
-------               ---------------
Failed                Proj.IntegrationTest.IntegrationTest21

在执行中没有Unittests程序集,测试通过。

> mstest.exe /testcontainer:IntegrationTests.dll

Loading C:\Proj\Tests\bin\x86\Release\Integrationtests.dll...
Starting execution...

Results               Top Level Tests
-------               ---------------
Passed                Proj.IntegrationTest.IntegrationTest21

我认为可能是在UnitTests dll上执行了[AssemblyInitialize]操作,或者是Unittest.dll中的某种静态状态或公共依赖项在加载测试程序集时发生了修改。但我在Unittests.dll中找不到任何静态构造函数或程序集初始化操作。我怀疑在包含Unittests程序集时存在部署差异(例如,依赖程序集以不同的版本部署等),但我已经比较了通过/失败的部署目录,并且它们是二进制等效的。

因此,Unittests程序集的哪个部分会导致测试结果的不同? 从单元测试中,我每次移除一半的测试,直到将其缩小到Unit tests程序集中的一个源文件。除了测试类之外,还声明了一个扩展方法:

除了这个扩展类之外,Unittest程序集现在只包含一个虚拟测试类中的单个测试用例。只有当我声明了一个虚拟测试方法并且声明了扩展方法时,才会出现测试失败。我可以删除所有剩余的测试逻辑,直到Unittest dll成为一个仅包含这些内容的单个文件:

// DummyTest.cs in Unittests.dll
[TestClass]
public class DummyTest
{
    [TestMethod]
    public void TestNothing()
    {
    }
}

public static class IEnumerableExtension
{
   public static IEnumerable<T> SymmetricDifference<T>(
       this IEnumerable<T> @this,         
       IEnumerable<T> that) 
   {
      return @this.Except(that).Concat(that.Except(@this));
   }
}

如果测试方法或扩展类被移除,测试将通过。两者同时存在时,测试将失败。

在任何程序集中都没有调用扩展方法,并且在集成测试执行之前,Unittests程序集中也没有执行任何代码(据我所知)。

我确定集成测试足够复杂,即使是JIT优化上的差异也可能导致浮点数值的差异。这就是我看到的吗?


我不认为Extensions类会引起任何问题。它最终只是一种语法糖,在编译时被替换为:var1.SymmetricDifference(var2) 被替换为 IEnumerableExtension.SymmetricDifference(var1, var2) - Dominic Zukiewicz
事件日志中没有任何内容,唯一的异常是AssertFailed,它来自于测试中浮点结果的比较。我无法生成问题的最小复现。 - Anders Forsgren
问题只在优化构建中出现,扩展方法的内容似乎并不重要,只要它的程序集被加载即可。测试从未调用它。 - Anders Forsgren
@Anders Forsgren:“只要程序集被加载”,那么加载程序集实际上是有什么区别的吗? - AVee
是的,如果它被加载并包含扩展方法,其行为会发生变化。我怀疑删除测试方法将阻止其加载,而删除扩展方法则会以某种方式防止优化差异。 - Anders Forsgren
显示剩余2条评论
3个回答

1
可能是由于类型加载错误导致的问题。
当CLR运行时加载类或方法时,它总是检查这些项中使用的所有类型。无论是否实际调用类型/方法都无关紧要。重返你的样本,扩展方法SymmetricDifference声明它使用System.Core程序集中的Except和Concat方法。
发生错误的是从System.Core程序集加载类型System.Linq.Enumerable时出现的错误。
造成这种行为的原因可能各不相同。首先要采取的措施是记录测试失败时得到的确切异常。

感谢您的建议,我没有看到任何异常(没有TypeLoaderExceptions等)。如果需要,我可以使用扩展方法,它能按预期工作。测试失败看起来像是正常的测试失败,有一个比较差异,可能由于浮点数差异引起的。 - Anders Forsgren
mstest.exe 可以生成 .trx 文件。在测试失败后,您可以打开生成的 .trx 文件,查看确切的错误和位置。 - ogggre
这是一个简单的“正常”测试断言失败,例如“期望4个苹果但却只找到3个”。测试结果中没有异常。 - Anders Forsgren

0

两个想法...

  1. 浮点数运算和比较在优化/未优化的情况下实际上可能会产生不同的结果。至少在 C++ 中是这样的(链接1)。它们也可能会在不同的 CPU 架构上产生不同的结果。一般来说,根据浮点数比较来判断单元测试是否通过听起来像是一个坏主意。例如,你的朋友在运行 x64 windows 上失败了单元测试,而你却成功了。

  2. 这个扩展方法问题是一次徒劳无功的追逐。如果你删除它,它神奇地工作,那又怎样呢?优化后的代码有时候会做出疯狂的事情,但最终问题在于你的浮点数与未优化时不同。如果必须要有一些差异,可以添加一个极小值,否则就要在每一步计算时记录它的值并追踪下去。


我知道优化会导致浮点行为的差异,特别是在x86上(在x64上不太明显,因为JIT倾向于使用SSE)。我的问题不是“如何解决这个问题”(我可以避免测试),而是我很好奇为什么当存在扩展方法时,优化似乎会以不同的方式进行。我可以确定差异发生在是否有扩展方法的情况下,那么问题就是:为什么? - Anders Forsgren

0

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