XmlSerializer在64位系统上启动时性能损失巨大

22

在调用一个拥有大量字段的类进行简单的XmlSerializer.Deserizlize()操作时,我遇到了非常严重的性能损失。

注意:我是在家里没有使用Visual Studio编写代码,所以可能会出现一些错误。

我的可序列化类是扁平的,并且拥有数百个字段:

[Serializable]
class Foo
{
    public Foo() { }

    [XmlElement(ElementName = "Field1")]
    public string Field1;

    // [...] 500 Fields defined in the same way

    [XmlElement(ElementName = "Field500")]
    public string Field500;
}

我的应用程序反序列化一个输入字符串(甚至是小字符串):

 StringReader sr = new StringReader(@"<Foo><Field1>foo</Field1></Foo>");
 XmlSerializer serializer = new XmlSerializer(typeof(Foo));
 object o = serializer.Deserialize(sr);

在32位系统中运行应用程序(或使用corflags.exe强制为32位),第一次执行代码需要约1秒钟(生成临时序列化类,等等...),然后接近于0。

在64位系统中运行应用程序,第一次执行代码需要1分钟,然后接近于0。

是什么导致了这么长时间的系统挂起,在64位系统中首次执行XmlSerializer对于一个大类进行序列化时?

现在我不确定是要归咎于临时类的生成/删除、xml名称表初始化、CAS、Windows搜索、防病毒软件还是圣诞老人...

剧透警告

这里是我的测试结果,请勿阅读如果不想被我的(可能)分析错误转移注意力。

  • 在Visual Studio调试器中运行代码使得代码在64位系统下也运行快速
  • 添加(完全没有文档说明的)system.diagnostic开关"XmlSerialization.Compile",它会防止系统删除序列化临时类,使得代码即使在64位系统下也运行快速
  • 获取运行时创建的临时FooXmlSerializer类,包括.cs文件在我的项目中,使用它代替XmlSerializer,使得代码即使在64位系统下也运行快速
  • 使用sgen.exe创建相同的FooXmlSerializer类,包括.cs文件在我的项目中,使用它代替XmlSerializer,使得代码即使在64位系统下也运行快速
  • 使用sgen.exe创建相同的FooXmlSerializer类,将Foo.XmlSerializers.dll程序集引用到我的项目中,并使用它代替XmlSerializer,使得代码在64位系统中运行缓慢(这让我非常困扰
  • 仅当输入的反序列化内容实际包含大类的字段时,性能损失才会发生(这也让我非常困扰

更进一步解释最后一点,如果我有一个类:

[Serializable]
class Bar
{
    public Bar() { }

    [XmlElement(ElementName = "Foo")]
    public Foo Foo; // my class with 500 fields
}

只有在传递一个Foo子对象时,反序列化才会变慢。即使我已经执行了反序列化操作:

 StringReader sr = new StringReader(@"<Bar></Bar>");
 XmlSerializer serializer = new XmlSerializer(typeof(Bar));
 object o = serializer.Deserialize(sr); // FAST

 StringReader sr = new StringReader(@"<Bar><Foo><Field1>foo</Field1></Foo></Bar>");
 XmlSerializer serializer = new XmlSerializer(typeof(Bar));
 object o = serializer.Deserialize(sr); // SLOW

编辑 我忘了说,我用进程监视器分析了执行过程,我没有看到任何来自我的应用程序或csc.exe的长时间任务,也没有看到任何与框架相关的内容。系统只是在做其他事情(或者我漏掉了什么),比如杀毒软件、explorer.exe、Windows搜索索引(已经尝试关闭它们)。


我的Foo()类和调用XmlDeserializer的代码是由一个旧系统动态生成的,我不能进行太多重构。我已经有了一个可接受的解决方案(使用system.diagnostic开关设置),但我真的很想知道是什么导致了系统的挂起 :) - Filini
类 Foo 由于其是内部的而非公开的(在您的示例中),因此无法进行反序列化。在我的系统上,32位发布版本构建需要0.055秒来执行反序列化。与您的结果一样,我的64位发布版本需要12.5秒!因此,在那里减速了约227倍。 - Jesse C. Slicer
杰西,我的Foo类在我的真实代码中是公共的。我在家里写的,没有用Visual Studio,我没有写完整... - Filini
如果您在类中添加更多字段,您会发现时间增长了。然后添加"XmlSerialization.Compile"开关,看它运行得快 :) - Filini
1
+1 希望这个问题能够得到一些专业的关注。这是一个有趣的情况。 - John K
显示剩余5条评论
4个回答

9
我不知道这是否有关系,但我遇到了XSLT问题,并在Microsoft的那些相当有趣的评论中发现以下内容:

问题的根源与两件事有关:首先,x64 JIT编译器有一些算法是二次扩展的。不幸的是,其中一个是调试信息生成器。因此,对于非常大的方法,它确实失控了。

[...]

64位JIT中还有一些多项式扩展的算法。我们正在努力将32位JIT编译器移植到x64,但这将在下一个并行运行时版本发布之前看到曙光(例如“2.0和4.0并行运行,但3.0 / 3.5 / 3.5SP1是“原地”发布)。我将其切换为“建议”,以便我可以将其附加到JIT吞吐量工作项上,以确保在新移植的JIT准备好发货时进行修复。

再次强调,这是完全不同的问题,但我认为64位JITter的评论是普遍的。

你说得对,这是一个不同的问题,但64位JITter性能问题可能会有关联。 - Filini
同意,JIT在某种程度上对此负有责任,我不知道为什么,但它确实如此! - massimogentilini

6

更新:

我成功地重现了这个问题,调查显示大部分时间都花在JIT编译器上:

JittingStarted: "Microsoft.Xml.Serialization.GeneratedAssembly.XmlSerializationReaderFoo", "Read2_Foo", "instance class SerializersTester.Foo"

你可以很容易地证明这一点,无需使用任何性能分析工具。

  • 通过sgen为x86和x64目标生成*.XmlSerializers.dll
  • 通过ngen生成本机映像。

你会发现与x86汇编相比,x64生成速度要慢得多

确切的原因隐藏在x64 JIT内部(顺便说一句,它与x86完全不同),不幸的是我没有足够的空闲时间去找到它。

为了避免这种性能损失,您可以通过sgen生成序列化程序集,引用它并在最终用户PC上的应用程序设置期间通过ngen编译为本机映像。


同时发生在.NET 2.0、3.5和4.0版本中。"生成序列化程序集"只为Web服务引用生成序列化类。我手动使用了sgen.exe,可以参考我的帖子。还没有使用过性能分析工具。 - Filini
1
“确切的原因隐藏在x64 JIT内部” - 希望这个主题能吸引JIT团队的成员,他们可以为此提供一些启示。 - Jesse C. Slicer
Nick,我仍然不明白的是,为什么从外部程序集引用的Microsoft.Xml.Serialization.GeneratedAssembly.XmlSerializationReaderFoo类很慢,而将其添加到项目中却很快。将foo类及其序列化器类放在同一个程序集中是否有助于JIT编译器的优化? - Filini
1
@Filini,如果不深入了解64位Jitter内部,我无法给出确切的答案,我只能在这里猜测。也许你应该将这个问题发布到http://connect.microsoft.com/。x64 JIT编译器团队对它的确切工作原理了解得更多。 - Nick Martyshchenko
+1 for "generate serializer's assembly via sgen, reference it and compile to native image via ngen". 这是一个不错的临时解决方案,但我仍然希望 JIT 团队成员能看到这个问题。 - Jesse C. Slicer

3

为了澄清 "XmlSerialization.compile",这是它的作用:

如果我们在64位环境下运行没有.config文件的代码,它会变得很慢。

如果我们把以下部分添加到应用程序的.config文件中:

<configuration>
   <system.diagnostics>
     <switches>
        <add name="XmlSerialization.Compilation" value="4"/>
     </switches>
   </system.diagnostics>
</configuration>

结果如下:
  • 序列化器的.cs文件、DLL文件和PDB文件被留在临时文件夹中。
  • 序列化器启动很快,虽然比32位版本慢,但是绝对可以接受(1-2秒而不是60秒)。

也许以调试模式创建DLL文件(因为有PDB文件可用)可以改变JIT编译器的行为,使其再次变快...


0

自从64位.NET发布以来,微软就已经知道这个问题:

http://connect.microsoft.com/VisualStudio/feedback/details/508748/memory-consumption-alot-higher-on-x64-for-xslcompiledtransform-transform-then-on-x86

来自 MSFT: “x64 JIT 编译器有一些算法呈二次方比例扩展。……自从 2005 年首次发布 64 位框架以来,我们已经看到了这种情况多次。”

“这个问题有两个方面:a) 已知;b) 不太容易解决。这是 64 位 JIT 的设计问题。我们正在早期阶段替换我们的 64 位 JIT 实现,所以它最终会被解决,但是不幸的是不会在 CLR 4.0 时间范围内得到解决。”


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