【问题标题】:Can an XmlSerializer pool strings to avoid large duplicate strings?XmlSerializer 可以池字符串以避免大的重复字符串吗?
【发布时间】:2009-04-03 09:22:19
【问题描述】:

我有一些非常大的 XML 文件,我使用 System.Xml.Serialization.XmlSerializer 读取这些文件。它非常快(嗯,足够快),但我希望它汇集字符串,因为一些长字符串出现很多次。

XML 看起来有点像这样:

<Report>
    <Row>
        <Column name="A long column name!">hey</Column>
        <Column name="Another long column name!!">7</Column>
        <Column name="A third freaking long column name!!!">hax</Column>
        <Column name="Holy cow, can column names really be this long!?">true</Column>
    </Row>
    <Row>
        <Column name="A long column name!">yo</Column>
        <Column name="Another long column name!!">53</Column>
        <Column name="A third freaking long column name!!!">omg</Column>
        <Column name="Holy cow, can column names really be this long!?">true</Column>
    </Row>
    <!-- ... ~200k more rows go here... -->
</Report>

XML 被反序列化成的类看起来有点像这样:

class Report 
{
    public Row[] Rows { get; set; }
}
class Row 
{
    public Column[] Columns { get; set; }
}
class Column 
{
    public string Name { get; set; }
    public string Value { get; set; }
}

导入数据时,会为每个列名分配一个新字符串。我知道为什么会这样,但根据我的计算,这意味着一些重复的字符串占导入数据使用的内存的大约 50%。我认为花费一些额外的 CPU 周期将内存消耗减少一半是一个很好的权衡。有什么方法可以让XmlSerializer 池字符串,以便丢弃重复项并在下次发生 gen0 GC 时回收?


还有一些最后的说明:

  • 我无法更改 XML 架构。它是从第三方供应商导出的文件。

  • 我知道(理论上)可以使用 XmlReader 来创建一个更快的解析器,它不仅可以让我进行自己的字符串池,还可以在导入过程中处理数据,这样不是所有的在我读完整个文件之前,必须将 200k 行保存在 RAM 中。不过,我宁愿不花时间编写和调试自定义解析器。真正的 XML 比示例要复杂一些,因此这是一项非常重要的任务。如上所述 - XmlSerializer 的性能确实足以满足我的目的,我只是想知道是否有一种简单的方法可以稍微调整一下。

  • 我可以编写自己的字符串池并在 Column.Name 设置器中使用它,但我不希望 (1) 这意味着摆弄自动生成的代码,并且 (2) 它会打开解决与并发和内存泄漏相关的一系列问题。

  • 不,我所说的“池化”并不是指“实习”,因为这会导致内存泄漏。

【问题讨论】:

  • 实习有什么问题?这只是一个系统管理的字符串池(编译器也使用)。
  • 我希望池在反序列化完成后被丢弃,所以我不会占用比我需要的更多的内存。一个实习字符串“从不”释放(“在公共语言运行时 (CLR) 终止之前,为实习字符串对象分配的内存不可能被释放”,API 说)。
  • 谢谢...建议用那些有点晦涩的信息替换“愚蠢”。

标签: c# .net xml-serialization memory-management


【解决方案1】:

就个人而言,我会毫不犹豫地手动启动实体 - 或者通过假设生成代码的所有权,或者手动执行(并摆脱数组;-p)。

重新并发 - 你可能有一个线程静态池? AFAIK,XmlSerializer 只使用一个线程,所以这应该没问题。它还可以让您在完成后将游泳池扔掉。那么你可以拥有一个类似的静态池,但是每个线程。然后也许调整设置器:

class Column 
{
    private string name, value;
    public string Name {
       get { return this.name; }
       set { this.name= MyPool.Get(value); }
    }
    public string Value{
       get { return this.value; }
       set { this.value = MyPool.Get(value); }
    }
}

静态MyPool.Get 方法与用[ThreadStatic] 装饰的静态字段(大概是HashSet&lt;string&gt;)对话。

【讨论】:

  • 我考虑过这样做。这可能是一种不错的方法,但我希望有一个更优雅的解决方案,这不需要我记住某个地方隐藏了一个字符串池:)
【解决方案2】:

我知道它的旧线程,但我找到了一个很好的方法:

创建覆盖Value 属性的XmlReader,在返回值之前检查它是否存在于字符串池中,然后返回它。

来自msdnXmlReaderValue 属性:

返回的值取决于节点的 NodeType。以下 表列出了具有要返回的值的节点类型。所有其他节点 类型返回 String.Empty。

例如,对于Attribute NodeType,它返回属性的值。

因此实现将如下所示:

public class StringPoolXmlTextReader : XmlTextReader
{
    private readonly Dictionary<string, string> stringPool = new Dictionary<string, string>();

    internal StringPoolXmlTextReader(Stream stream)
        : base(stream)
    {
    }

    public override string Value
    {
        get
        {
            if (this.NodeType == XmlNodeType.Attribute)
                return GetOrAddFromPool(base.Value);

            return base.Value;
        }
    }

    private string GetOrAddFromPool(string str)
    {
        if (str == null)
            return null;

        if (stringPool.TryGetValue(str, out var res) == false)
        {
            res = str;
            stringPool.Add(str, str);
        }

        return res;
    }
}

使用方法:

using (var stream = File.Open(@"..\..\Report.xml", FileMode.Open))
{
   var reader = new StringPoolXmlTextReader(stream);
   var ser = new XmlSerializer(typeof(Report));
   var data = (Report)ser.Deserialize(reader);
}

性能:我检查了具有随机列值的 200K 行的性能,发现反序列化时间相同,Report 内存从 78,551,460 字节下降到 48,890,016 字节(减少约 38%)。

注意事项:

  1. 示例继承自XmlTextReader,但您可以继承自任何XmlReader
  2. 您也可以通过像这样public override string Value =&gt; GetOrAddFromPool(base.Value); 覆盖Value 属性来将字符串池用于列值,但是当值不重复时(例如在我的测试中它们是随机的),它可以将反序列化时间增加约20% .

【讨论】:

    【解决方案3】:

    我建议您不要对此进行预优化。等到它工作,分析结果,然后根据分析结果进行优化。您可能会发现首先要进行一些其他优化。

    【讨论】:

      【解决方案4】:

      如果您使用 DataContract 序列化程序(如 WCF 使用的那样)而不是使用 XmlSerializer,则可以使用 OnDeserializedAttribute 定义在实例反序列化后调用的方法。

      或者,如果 XML 没有比示例复杂得多,那么为什么不通过 XmlReader 实现自己的反序列化。

      【讨论】:

      • 查看stackoverflow.com/questions/644964,检查过OP,他们确实被调用了。
      • @Marc:正确...所以不确定另一个问题发生了什么。所以现在有两种不同的选择。
      • @Richard;不,他们真的没有。仅适用于 DataContractSerializer。
      猜你喜欢
      • 2016-06-17
      • 1970-01-01
      • 1970-01-01
      • 2011-07-01
      • 1970-01-01
      • 2010-09-13
      • 2013-07-31
      • 2018-10-15
      • 1970-01-01
      相关资源
      最近更新 更多