关于 XmlSerializer 性能和 Sgen





5.00/5 (6投票s)
提高 XmlSerialization 的性能。
介绍
许多(如果不是全部)企业项目都使用序列化器,而且有很多选择,但我们倾向于默认使用内置的,然后我们倾向于依赖老式的 XML,因为它更具可读性和可编辑性,还有什么比将对象直接交给 XmlSerializer 并获得快速结果更简单呢?
序列化器也用于框架对象和许多第三方组件中,用于配置、状态持久化和通过通信通道发送数据。
但是然后你就遇到了性能问题……
背景
我长期以来一直是 .NET 领域的性能优化者,最近(又一次)解决了软件启动缓慢的问题,其中 XmlSerializer 的临时程序集生成占据了很大一部分。
所以我想,为什么不把它写下来(可以说),为自己(以及他人)提供一个可以回顾的资源……
问题
XmlSerializer 为包含一个或多个需要序列化的类型的每个程序集生成并编译一个临时程序集。这每个目标程序集只发生一次,但这可能会在应用程序启动时间(以及其他首次操作测量)上造成恼人的性能损失。
使用另一个序列化器
市面上有很多优秀的序列化器,包括内置的和第三方的。
- Protobuf.net
- 快速 JSON 序列化器(Service Stack)
- BinaryFormatter
这里有一些很好的基准测试:
缺点
- 您可能无法使用它们,因为您不想破坏对先前版本的支持,或者出于其他原因必须使用此特定格式。
如果这一点不是问题,您应该将 XmlSerializer 替换为上述之一,然后就可以解决了。
Sgen
sgen 听起来是个不错的选择,因为它允许您保留 XmlSerializer 及其灵活性,而无需在序列化对象发生更改时修改代码。
您只需添加一个生成后步骤,在编译时创建序列化程序集。
问题 + 解决方案:
- 您无法 sgen 一个程序集(整个),除非您拥有它所依赖的所有程序集(引用)。
- 解决方案:使用 /t: 并仅指定需要 sgen 的类型。
- 解决方案:在构建完成后对完整的 bin 目录使用 sgen。
- 解决方案:将调试器(Visual Studio 也可以)附加到 sgen 控制台并捕获异常。
- 这可以通过以下方式完成:
- 创建一个新的命令行项目。
- 转到项目属性 -> 调试。
- 添加 sgen 命令(使用完整路径)。
- 允许所有异常(CLR + cpp)。
- 运行(F5)。
- 您无法有效地 sgen 标准 .NET 集合类型,因为要 sgen List<string>,您需要 sgen *mscorlib.dll*,它具有 MS 签名。 - 这可以通过创建一个继承 `List<string>` 的自定义集合类来处理。
- 解决方案:创建一个自定义集合(非泛型)。
- 您必须 sgen 包含被序列化类型的*所有*程序集。
- 解决方案:只需在生成后步骤/Jenkins 命令行构建步骤中完成。
- 如果您使用强名称,则必须使用与原始程序集相同的密钥对 XmlSerializers 程序集进行签名。(这可能对第三方程序集有问题,除非它们是开源的)。
- 解决方案
- 在 vs2010 中,使用编译器标志:/c:/keyfile:"C:\somewhere\keyfile.snk"
- 在 vs2012 中,使用内置命令行标志:/keyfile:"C:\somewhere\keyfile.snk"
- XAML 生成的命名空间问题(ildasm,重命名,ilasm 带密钥/使用特定类型)
- 类型 'XamlGeneratedNamespace.GeneratedInternalTypeHelper' 同时存在于 'lib1.dll' 和 'lib2.dll' 中。
- 使用 ilasm ildasm 或 ilMerge 无济于事(XmlSerializer 执行 id 检查)。
- 解决方案:使用 /t: 选择类型将有助于避免此问题。
- 泛型类型*根本*不支持。
- 解决方案:如果您想使用 XmlSerializer 并避免临时程序集生成,您将不得不放弃序列化泛型类型。
- 私有/内部访问器问题
- 解决方案:使用特定类型 /t:
- 解决方案:如果可以的话,将它们设为 public。
- 解决方案:使用 `internalsVisibleTo`,并将 XmlSerializer 程序集添加为友元程序集。
- 仅对内部类型有效(私有类型仍然是私有的)。
- 使用 sn.exe 获取完整公钥(从您的程序集中):
- 这没关系,因为 XmlSerializer 程序集将使用相同的密钥签名。参见(强名称)。
- 打开 Visual Studio 命令提示符。
- 从程序集中获取公钥:sn.exe -Tp <assembly>
- 在项目的 *assemblyInfo.cs* 中添加:
[assembly: InternalsVisibleTo("YourAssembly.XmlSerializers,
PublicKey=0024000004800000940000000602000000240000525341310004000001000100f1844bc8cbdc
3779b0e5970a30d800668414128135f5d6cd274e726f7c84f234234324234c64c11d0f6a9edbbe7b32
b6f19d8f734e1c130814d40df54ff9d063ce29bf7af86b46a69f0e2342343241b52a2ae443648e199a0
9547e74663cbe1e72e89365034ff53b6a3ce281415cbe7e2dfb5e40e54667f35dc04ca")]
MSBuild 中的 Sgen
将 sgen 添加到简单项目中可能就像编写一个生成后步骤并添加
sgen /a:MyAssemblyName.dll /t:MyNamespace.MyType /c:/keyfile:"c:\directory\keyfile.snk" /f
但是将 sgen 添加到企业构建流程中可能会很棘手……由于大多数 .NET 构建环境最终都会使用 MSBuild,因此有一个内置 MSBuild 任务供您添加。
<Target Name="PostCompile">
<Sgen ShouldGenerateSerializer="true" UseProxyTypes="false" BuildAssemblyName="MYAssembly.dll" BuildAssemblyPath="..\bin\" Types="MynameSpace.MyTypeName" KeyFile="..\dir\keyfile.snk" />
</Target>
如果存在多个类型
<ItemGroup>
<SgenTypes Include="MyNamespace1.MyTypename1" />
<SgenTypes Include="MyNamespace2.MyTypename2" />
</ItemGroup>
<Target Name="PostCompile">
<Sgen ShouldGenerateSerializer="true" UseProxyTypes="false" BuildAssemblyName="MYAssembly.dll" BuildAssemblyPath="..\bin\" Types="@(SgenTypes)" KeyFile="..\dir\keyfile.snk" />
</Target>
问题
MSBuild 中使用的默认 ToolsVersion 几乎总是 2.0,并且此工具版本附带的 Sgen 任务没有 Types="" 属性。正如我们之前所见,选择类型是解决许多问题的关键,所以我们需要使用更新的工具版本……
- 解决方案:通过在文件顶部的 project 标签中添加 ToolsVersion="4.0" 来更改版本。
- 问题:可能与习惯了旧工具和行为的现有步骤产生冲突。
- 解决方案:仅更改 sgen 目标以使用新的工具版本。
- 为此,我们需要将前面的几行提取到另一个项目文件中。
- 导入命令将无济于事,因为它会忽略新文件中的 ToolsVersion。
- 请改用以下命令:
在新文件中(sgen.proj)
<Project DefaultTargets="PostCompile" xmlns="http://schemas.microsoft.com/developer/msbuild/2003" ToolsVersion="4.0">
<ItemGroup>
<SgenTypes Include="MyNamespace1.MyTypename1" />
<SgenTypes Include="MyNamespace2.MyTypename2" />
</ItemGroup>
<Target Name="PostCompile" >
<Sgen ShouldGenerateSerializer="true" UseProxyTypes="false" BuildAssemblyName="MYAssembly.dll" BuildAssemblyPath="..\bin\" Types="@(SgenTypes)" KeyFile="..\dir\keyfile.snk" />
</Target>
</Project>
在主 msproject 文件中
<MSBuild Projects="sgen.proj" ToolsVersion="4.0" Targets="PostCompile" />
这将告诉 MSBuild 运行另一个实例,加载新的 ToolsVersion,并调用目标,这次使用 4.0 版本的工具,该工具包含了 <sgen> 标签上的 Types 属性。
实现 IXmlSerializable
虽然我听说使用此方法可以阻止 `XmlSerializer` 生成临时程序集,并且我觉得手动覆盖应该在任何类型被生成之前执行,但我发现事实并非如此,它会生成临时程序集,然后从生成的代码中运行 `ReadXml`/`WriteXml`。
使用此方法,您实际上不会获得性能提升,也不会为给定类型使用 `XmlSerializer`。
使用 ilSpy 查看框架代码,我们可以调用调用 IXmlSerializable 实现的函数,并使用它们来绕过和模仿框架。
为了获得性能,请直接从您的基础设施代码中调用实现 IXmlSerializable 的对象的 IXmlSerializable 实现。
ReadSerializable:
protected IXmlSerializable ReadSerializable(IXmlSerializable serializable, bool wrappedAny)
{
string b = null;
string b2 = null;
if (wrappedAny)
{
b = this.r.LocalName;
b2 = this.r.NamespaceURI;
this.r.Read();
this.r.MoveToContent();
}
serializable.ReadXml(this.r);
if (wrappedAny)
{
while (this.r.NodeType == XmlNodeType.Whitespace)
{
this.r.Skip();
}
if (this.r.NodeType == XmlNodeType.None)
{
this.r.Skip();
}
if (this.r.NodeType == XmlNodeType.EndElement &&
this.r.LocalName == b && this.r.NamespaceURI == b2)
{
this.Reader.Read();
}
}
return serializable;
}
WriteSerializable:
/// <summary>Instructs <see cref="T:System.Xml.XmlNode" /> to write an object that uses custom XML formatting as an XML element. </summary>
/// <param name="serializable">An object that implements the <see cref="T:System.Xml.Serialization.IXmlSerializable" /> interface that uses custom XML formatting.</param>
/// <param name="name">The local name of the XML element to write.</param>
/// <param name="ns">The namespace of the XML element to write.</param>
/// <param name="isNullable">true to write an xsi:nil='true' attribute if the <see cref="T:System.Xml.Serialization.IXmlSerializable" /> object is null; otherwise, false.</param>
/// <param name="wrapped">true to ignore writing the opening element tag; otherwise, false to write the opening element tag.</param>
protected void WriteSerializable(IXmlSerializable serializable, string name, string ns, bool isNullable, bool wrapped)
{
if (serializable == null)
{
if (isNullable)
{
this.WriteNullTagLiteral(name, ns);
}
return;
}
if (wrapped)
{
this.w.WriteStartElement(name, ns);
}
serializable.WriteXml(this.w);
if (wrapped)
{
this.w.WriteEndElement();
}
}
protected void WriteNullTagLiteral(string name, string ns)
{
if (name == null || name.Length == 0)
{
return;
}
this.WriteStartElement(name, ns, null, false);
this.w.WriteAttributeString("nil", "http://www.w3.org/2001/XMLSchema-instance", "true");
仔细检查 ReadSerializable,我们可以看到 wrappedAny 通常为 false,正如在实际调用的函数(只有一个参数的 ReadSerializable)中所示。
return this.ReadSerializable(serializable, false);
因此,代码被简化为对 IXmlSerializable 的 ReadXml 函数的简单调用。
WriteSeializable 有更多代码,经过检查,在生成的程序集中创建的默认生成如下:
WriteSerializable((System.Xml.Serialization.IXmlSerializable)((global::MyNamespace.MyClass)a[ia]), @"GeneratedSomethingLikeMyClassname", @"", true, true);
- wrapped = true
- nullable = true
- ns = ""
- name = ???
我的 WriteSerializable 版本(如下)包含了 wrapped、nullable 和 namespace 的这些默认值,以及一些用于获取名称的检测工作,包括 XmlRoot 属性以及列表和数组的命名,但您可以根据需要进行更改,添加对 XmlArray、XmlArrayItem 属性的支持(或者只将名称传递给函数并删除所有名称创建代码)。
public void WriteSerializable(IXmlSerializable serializable, XmlWriter writer, string name = null, string ns = "", bool isNullable = true, bool wrapped = true)
{
if (name == null)
{
name = serializable.GetType().Name;
Type t = serializable.GetType();
if (typeof(IList).IsAssignableFrom(t) && t.IsGenericType)
{
name = "ArrayOf" + t.GetGenericArguments()[0].Name;
}
else if (t == typeof(Array) || t == typeof(ArrayList))
{
name = "ArrayOfObject";
}
else
{
object[] attribs = serializable.GetType().GetCustomAttributes(typeof(XmlRootAttribute), false);
if (attribs.Length > 0)
{
XmlRootAttribute xmlRoot = attribs[0] as XmlRootAttribute;
name = xmlRoot.ElementName;
}
}
}
if (serializable == null)
{
if (isNullable)
{
if (name == null || name.Length == 0)
{
return;
}
writer.WriteStartElement(name, ns);
writer.WriteAttributeString("nil",
"http://www.w3.org/2001/XMLSchema-instance", "true");
writer.WriteEndElement();
}
return;
}
if (wrapped)
{
writer.WriteStartElement(name, ns);
}
serializable.WriteXml(writer);
if (wrapped)
{
writer.WriteEndElement();
}
}
手动创建 XML
如果您只需要少量数据,您始终可以使用 `XElement`/`XmlWriter` 来创建/读取 XML,这具有速度快、与先前格式 100% 向后兼容且完全可定制的优点。
缺点
- 代码越多,bug 越多。
- 添加数据字段时需要维护。
- 如果您(以及您项目中的其他使用者)正在使用底层使用 `XmlSerializer` 的通用框架,则不可行。
兴趣点
配置 XmlSerializer 以显示日志并将临时程序集 + .cs 文件保留在磁盘上
如果我们想了解更多关于它的操作信息,可以配置 `XmlSerializer` 以保留配置(将此添加到您的 *app.config* / *web.config* 文件中的 configuration 标签下)。
<system.diagnostics>
<switches>
<add name="XmlSerialization.PregenEventLog" value="1" />
<add name="XmlSerialization.Compilation" value="1" />
</switches>
</system.diagnostics>
使用这些标志,您将能够获取 `XmlSerializer` 的代码和临时程序集,它们将位于临时文件夹或配置的目标文件夹中(只需输入:**%temp%**)。
`XmlSerializer` 会将日志写入 Windows 事件日志,因此要查看它,您只需
- 开始菜单,运行(WinKey+r):
- eventvwr
- 转到应用程序日志。
要更改临时程序集生成的*目标*文件夹,请将此添加到您的配置(*app.config*/*web.config*):
<system.xml.serialization>
<xmlSerializer tempFilesLocation="c:\\foo"/>
</system.xml.serialization>
发现 Xmlserializer.dll 是否被加载
您可以使用 fuslogvw 来跟踪 DLL 加载过程。
- 打开 Visual Studio 命令提示符。
- 输入:**fuslogvw**
- 将启动一个应用程序。
- 按设置。
- 选择 Log All binds to disk(您也可以使用“log bind failures”)。
- 勾选 enable custom log path。
- 在文本框中添加您自己的路径(确保它存在)。
用于此任务的其他工具
有几个工具试图让 C# 中的 XML 生成优化变得更容易,但它们都相当老旧(2007-2008 年)并且看起来停滞不前。
- Mvp.Xml.Xgen
- SgenPlus
- XGenPlus
如果您在网上找到更好的东西,请随时在评论中告诉我。此外,如果您有其他关于 sgen 问题的问题/解决方案,请告诉我,我会添加到这里。