Linq性能解析XML文件

本文关键字:XML 文件 性能 Linq | 更新日期: 2023-09-27 18:01:36

我需要解析一个XML文件(1~10 MB);我使用XDocument的目的。

目前我使用Linq来查询XML文档,但我需要提高我的应用程序的性能,我想用老式循环取代Linq查询,但我没有任何提升。

下面是最常用的查询代码:

Stopwatch stopwatch = new Stopwatch();
XDocument xdoc = XDocument.Load("filename.xml");
string def = "Version";
XElement xelm;
stopwatch.Start();
for (int i = 0; i < 1000; i++)
    xelm = xdoc.Descendants("def").Where(d => d.Attribute("name").Value == def).Single();
stopwatch.Stop();
Console.WriteLine(stopwatch.ElapsedMilliseconds);

stopwatch.Restart();
for (int i = 0; i < 1000; i++)
{
    foreach (var elm in xdoc.Descendants("def"))
    {
        if (elm.Attribute("name").Value == def)
        {
            xelm = elm;
            break;
        }
    }
}
stopwatch.Stop();
Console.WriteLine(stopwatch.ElapsedMilliseconds);

两个版本的运行时间几乎相同,对我来说,这个结果很奇怪,因为我认为LinqWhere方法在调用时必须创建一个新列表。

为什么两个版本有相同的性能?有办法改进原始代码吗?

Linq性能解析XML文件

LINQ使用延迟(延迟)执行,这意味着它只在需要时迭代集合一次

当你必须操作大型数据集合时,延迟执行可以极大地提高性能,特别是在包含一系列链式查询或操作的程序中。在最好的情况下,延迟执行只允许通过源集合进行一次迭代。

在这种情况下,没有从Where子句生成新的列表。事实上,编译后的LINQ语句将以与foreach语句几乎相同的方式运行。

编辑:我关于提高性能的唯一想法是遵循Robert McKee的答案,他说你应该使用.First()而不是.Single(),这样就不必迭代整个列表。除此之外,我不确定还能做什么,除了使用不同的库或不同的语言。

.Single()切换到.First(),性能应该会好得多。for循环几乎与.First()完全相同,而.Single更像是:

for (int i = 0; i < 1000; i++)
{
    foreach (var elm in xdoc.Descendants("def"))
    {
        if (elm.Attribute("name").Value == def)
        {
            if (xelm!=null)
                throw new InvalidOperationException();
            xelm = elm;
        }
    }
    if (xelm==null)
       throw new InvalidOperationException();
}

,它将继续遍历文档,直到找到另一个匹配(并抛出异常),或者到达文档的末尾。

你可以尝试使用PLINQ,但是你需要做大量的性能测试,因为有很多因素会影响PLINQ的性能变化,包括它所运行的硬件和文档的结构等。

stopwatch.Start();
for (int i = 0; i < 1000; i++)
    xelm = xdoc.AsParallel()
        .Descendants("def")
        .Where(d => d.Attribute("name").Value == def)
        .Single();
stopwatch.Stop();
Console.WriteLine(stopwatch.ElapsedMilliseconds);

总结一下你的情况:

  1. 在启动时加载一个大的XDocument
  2. 您通过XDocument运行许多线性搜索,以根据名称和属性值查找节点。
  3. 节点可以在加载后添加或删除。
  4. 重复线性搜索的性能太慢。

在这种情况下,似乎有必要将节点放在某种查找表中以减小搜索范围。这里的困难在于,文档在加载之后会发生变化,因此需要某种机制来保持表的最新状态。幸运的是,XDocument上的XObject.ChangingXObject.Changed可以用于此目的。

例如,以下类可用于按名称查找XElement节点,并在添加或删除节点或更改节点名称时动态更新其查找表:

public class XDocumentRepository
{
    readonly XDocument doc;
    readonly Dictionary<XName, List<XElement>> elementsByName = new Dictionary<XName, List<XElement>>();
    public XDocument Document { get { return doc; } }
    public IEnumerable<XElement> ElementsByName(XName name)
    {
        return elementsByName.Items(name);
    }
    public XDocumentRepository(XDocument doc)
    {
        if (doc == null)
            throw new ArgumentNullException();
        this.doc = doc;
        doc.Changing += new EventHandler<XObjectChangeEventArgs>(doc_Changing);
        doc.Changed += new EventHandler<XObjectChangeEventArgs>(doc_Changed);
        AddAll(doc.Root);
    }
    private void AddAll(XElement root)
    {
        foreach (var element in root.DescendantsAndSelf())
            elementsByName.AddItem(element.Name, element);
    }
    private void RemoveAll(XElement root)
    {
        foreach (var element in root.DescendantsAndSelf())
            elementsByName.RemoveItem(element.Name, element);
    }
    void doc_Changed(object sender, XObjectChangeEventArgs e)
    {
        XElement xSender = sender as XElement;
        if (xSender != null && xSender.Document == doc)
        {
            switch (e.ObjectChange)
            {
                case XObjectChange.Add:
                case XObjectChange.Remove:
                    AddAll(xSender);
                    break;
                case XObjectChange.Name:
                    elementsByName.AddItem(xSender.Name, xSender);
                    break;
                case XObjectChange.Value:
                    break;
                default:
                    Debug.Assert(false, "unknown ObjectChange");
                    break;
            }
        }
        // If an attribute value were changed, sender would be an XAttribute
    }
    void doc_Changing(object sender, XObjectChangeEventArgs e)
    {
        XElement xSender = sender as XElement;
        if (xSender != null)
        {
            switch (e.ObjectChange)
            {
                case XObjectChange.Add:
                case XObjectChange.Remove:
                    RemoveAll(xSender);
                    break;
                case XObjectChange.Name:
                    elementsByName.RemoveItem(xSender.Name, xSender);
                    break;
                case XObjectChange.Value:
                    break;
                default:
                    Debug.Assert(false, "unknown ObjectChange");
                    break;
            }
        }
        // If an attribute value were changed, sender would be an XAttribute
    }
}
public static class DictionaryExtensions
{
    public static void AddItem<TKey, TValueList, TValue>(this IDictionary<TKey, TValueList> listDictionary, TKey key, TValue value)
        where TValueList : IList<TValue>, new()
    {
        if (listDictionary == null)
            throw new ArgumentNullException();
        TValueList values;
        if (!listDictionary.TryGetValue(key, out values))
            listDictionary[key] = values = new TValueList();
        values.Add(value);
    }
    public static IEnumerable<TValue> Items<TKey, TValue>(this IDictionary<TKey, List<TValue>> listDictionary, TKey key)
    {
        if (listDictionary == null)
            throw new ArgumentNullException();
        List<TValue> list;
        if (!listDictionary.TryGetValue(key, out list))
            return Enumerable.Empty<TValue>();
        return list;
    }
    public static bool RemoveItem<TKey, TValueList, TValue>(this IDictionary<TKey, TValueList> listDictionary, TKey key, TValue value)
        where TValueList : IList<TValue>, new()
    {
        if (listDictionary == null)
            throw new ArgumentNullException();
        TValueList values;
        if (!listDictionary.TryGetValue(key, out values))
            return false;
        return values.Remove(value);
    }
}

这只处理按名称查询元素,但您也可以将此想法扩展到属性值。

然后像这样使用:

    public static void Test()
    {
        string filePath = @"filename.xml";
        var xdoc = XDocument.Load(filePath);
        var repository = new XDocumentRepository(xdoc);
        string defName = "def";
        string defValue = "Some Test Value For Querying";
        Test(repository, defName, defValue, null); // Assert element not found.
        var test = new XElement(defName, new XAttribute("name", defValue));
        // Find the first deepest node in the document, for testing purposes.
        var node = xdoc.Descendants().OrderByDescending(e => e.AncestorsAndSelf().Count()).First();
        node.Add(test);
        Test(repository, defName, defValue, test); // Assert element found
        test.Attribute("name").Value = "fobar";
        Test(repository, defName, defValue, null); // Assert element not found when attribute value is changed
        test.Attribute("name").Value = defValue;
        Test(repository, defName, defValue, test); // Assert element found when attribute value restored
        test.Name = "flubber";
        Test(repository, defName, defValue, null); // Assert element not found when name changed
        test.Name = defName;
        Test(repository, defName, defValue, test); // Assert element found when name restored
        var parent = test.Parent;
        var parentParent = parent.Parent;
        test.Remove();
        Test(repository, defName, defValue, null); // Assert element not found when removed.
        parent.Add(test);
        Test(repository, defName, defValue, test); // Assert element found when added back
        if (parentParent != null)
        {
            parent.Remove();
            Test(repository, defName, defValue, null); // Assert element not found when parent removed.
            parentParent.Add(parent);
            Test(repository, defName, defValue, test); // Assert element found when parent added back.
        }
    }
    static void Test(XDocumentRepository repository, string elementName, string attributeValue, XElement expectedValue)
    {
        var xelm1 = repository.Document.Descendants(elementName).Where(d => (string)d.Attribute("name") == attributeValue).SingleOrDefault();
        var xelm2 = repository.ElementsByName(elementName).Where(d => (string)d.Attribute("name") == attributeValue).SingleOrDefault();
        Debug.Assert(xelm1 == xelm2 && xelm1 == expectedValue); // No assert
    }

注意,因为使用的是字典,所以返回元素的顺序是未定义的。如果有多个元素匹配任何给定的查询,则可以使用XNode.CompareDocumentOrder将它们按文档顺序排序。

您可能想要尝试一下。我在我的机器上尝试了这个,我可以看到它是有益的,如果订单对你来说不是很重要,虽然我没有打印出被选中的元素。根据实际代码的不同,您可能需要使用锁来处理同步。

stopwatch.Restart();
Parallel.For(0, 1000, i =>
{
   foreach (var elm in xdoc.Descendants("def"))
   {
        if (elm.Attribute("name").Value == "def")
        {
               xelm = elm;
               break;
         }
    }
});
stopwatch.Stop();
Console.WriteLine(stopwatch.ElapsedMilliseconds);

请注意,并行行" foreach (var elm in xdoc.Descendants("def")) "使用Parallel。在我的机器上,Foreach似乎没有给出一个很好的性能,这可能是由于并行化的开销很高(我猜)。