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);
两个版本的运行时间几乎相同,对我来说,这个结果很奇怪,因为我认为Linq
的Where
方法在调用时必须创建一个新列表。
为什么两个版本有相同的性能?有办法改进原始代码吗?
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);
总结一下你的情况:
- 在启动时加载一个大的
XDocument
。 - 您通过
XDocument
运行许多线性搜索,以根据名称和属性值查找节点。 - 节点可以在加载后添加或删除。
- 重复线性搜索的性能太慢。
在这种情况下,似乎有必要将节点放在某种查找表中以减小搜索范围。这里的困难在于,文档在加载之后会发生变化,因此需要某种机制来保持表的最新状态。幸运的是,XDocument
上的XObject.Changing
和XObject.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似乎没有给出一个很好的性能,这可能是由于并行化的开销很高(我猜)。