XmlSerializer池字符串可以避免大的重复字符串吗?

时间:2009-04-03 09:22:19

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

我有一些非常大的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 setter中使用它,但我不想像(1)那样意味着摆弄自动生成的代码,(2)它打开了一系列与并发和内存泄漏相关的问题。

  • 不,通过“汇集”,我并不是指“实习”,因为这会导致内存泄漏。

4 个答案:

答案 0 :(得分:2)

就个人而言,我会毫不犹豫地动手实体 - 无论是假设所生成代码的所有权,还是手动执行(以及摆脱数组;-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方法与静态字段(HashSet<string>)对话,其中大概是[ThreadStatic]

答案 1 :(得分:1)

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

答案 2 :(得分:0)

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

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

答案 3 :(得分:0)

我知道它的旧线,但我找到了一个很好的方式,因为它:

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

来自msdnValue的{​​{1}}属性:

  

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

例如,对于XmlReader 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;
    }
}

性能:我检查了具有随机列值的200K行的性能,发现反序列化时间相同,并且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); } 内存从78,551,460字节减少到48,890,016字节(减少了约38%)。

注释:

  1. 该示例继承自Report,但您可以继承自任何XmlTextReader
  2. 您也可以使用XmlReader这样的覆盖值属性将字符串池用于列值,但是当值不重复时(例如在我的测试中,当它们重复时,它可以将反序列化时间增加大约20%)。随机)。