我有一些非常大的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)它打开了一系列与并发和内存泄漏相关的问题。
不,通过“汇集”,我并不是指“实习”,因为这会导致内存泄漏。
答案 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
属性,方法是在返回值之前,先检查其是否存在于字符串池中,然后返回。
来自msdn的Value
的{{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%)。
注释:
Report
,但您可以继承自任何XmlTextReader
XmlReader
这样的覆盖值属性将字符串池用于列值,但是当值不重复时(例如在我的测试中,当它们重复时,它可以将反序列化时间增加大约20%)。随机)。