.NET: XmlReader vs Linq to XML Performance

Trying to squeeze out some performance from XML parser, I’ve performed a few tests to compare XmlReader and XElement in C#. The results are below:

MethodEntryCountMeanErrorStdDevGen 0Gen 1Gen 2Allocated
XmlReader1022.71 μs13.99 μs0.767 μs3.87570.1221-15.94 KB
Linq1031.69 μs17.07 μs0.936 μs4.9438--20.38 KB
XmlReader100201.16 μs91.83 μs5.033 μs13.18361.7090-53.92 KB
Linq100262.43 μs117.71 μs6.452 μs22.46095.3711-92.08 KB
XmlReader10001,957.26 μs487.94 μs26.745 μs70.312535.1563-427.13 KB
Linq10002,760.14 μs1,338.66 μs73.376 μs128.906362.5000-802.53 KB
XmlReader1000026,045.06 μs7,497.77 μs410.978 μs750.0000312.500093.75004261.65 KB
Linq1000037,641.87 μs22,644.42 μs1,241.217 μs1375.0000562.5000187.50008008.76 KB
XmlReader100000267,986.78 μs226,860.08 μs12,434.967 μs7000.00002500.0000500.000041974.41 KB
Linq100000338,254.50 μs84,972.74 μs4,657.643 μs13000.00005000.00001000.000079473.88 KB

Summary: XmlReader is always more efficient, both in terms of memory consumption and raw CPU cycles. No matter the document is big or small. Even with 10 elements, the performance gain is massive.

Full testing code here:

#LINQPad optimize+

static string xml = @"<?xml version=""1.0"" encoding=""UTF-8""?>
<ListBucketResult>
	<Name>algresearch</Name>
	<Prefix/>
	<KeyCount>5</KeyCount>
	<MaxKeys>1000</MaxKeys>
	<IsTruncated>false</IsTruncated>
	@c
	
</ListBucketResult>

";

static string contents = @"<Contents>
		<Key>11d247ec-ca73-4ab9-a8ff-a5ffc446f8a4</Key>
		<LastModified>2021-02-25T10:29:26.000Z</LastModified>
		<ETag>&quot;5cc7bdc9132074539612aacde94d39ae&quot;</ETag>
		<Size>15</Size>
		<StorageClass>STANDARD</StorageClass>
	</Contents>";

void Main()
{
	Util.AutoScrollResults = true;
	BenchmarkRunner.Run<ParseBenchmark>();
}

public class IOEntry
{
	public IOEntry(string key)
	{
		Key = key;
	}
	
	public string Key { get; private set; }
	
	public DateTimeOffset? LastModificationTime { get; set; }
	
	public int Size { get; set; }
	
	public string ETag { get; set; }
	
	public string StorageClass { get; set; }
}

[ShortRunJob]
[MarkdownExporter]
//[SimpleJob]
//[RPlotExporter]
[MemoryDiagnoser]
//[DisassemblyDiagnoser]
public class ParseBenchmark
{
	public IReadOnlyCollection<IOEntry> ParseWithXmlReader(string xml, out string continuationToken)
	{
		continuationToken = null;
		var result = new List<IOEntry>();
		using (var sr = new StringReader(xml))
		{
			using (var xr = System.Xml.XmlReader.Create(sr))
			{
				while (xr.Read())
				{
					if (xr.NodeType == XmlNodeType.Element)
					{
						switch (xr.Name)
						{
							case "NextContinuationToken":
								break;
							case "Contents":
								string key = null;
								string lastMod = null;
								string eTag = null;
								string size = null;
								string storageClass = null;
								// read all the elements in this
								string en = null;
								while (xr.Read() && !(xr.NodeType == XmlNodeType.EndElement && xr.Name == "Contents"))
								{
									if (xr.NodeType == XmlNodeType.Element)
										en = xr.Name;
									else if (xr.NodeType == XmlNodeType.Text)
									{
										switch (en)
										{
											case "Key":
												key = xr.Value;
												break;
											case "LastModified":
												lastMod = xr.Value;
												break;
											case "ETag":
												eTag = xr.Value;
												break;
											case "Size":
												size = xr.Value;
												break;
											case "StorageClass":
												storageClass = xr.Value;
												break;
										}
									}
								}

								if (key != null)
								{
									var entry = new IOEntry(key)
									{
										LastModificationTime = DateTimeOffset.Parse(lastMod),
										Size = int.Parse(size),
										ETag = eTag,
										StorageClass = storageClass
									};
									result.Add(entry);
								}

								break;
						}
					}
				}
			}
		}

		return result;
	}
	
	public IReadOnlyCollection<IOEntry> ParseWithXElement(string xml, out string continuationToken)
	{
		continuationToken = null;
		var result = new List<IOEntry>();
		
		XElement x = XElement.Parse(xml);
		continuationToken = x.Element("NextContinuationToken")?.Value;
		
		foreach(XElement c in x.Elements("Contents"))
		{
			var entry = new IOEntry(c.Element("Key").Value)
			{
				LastModificationTime = DateTimeOffset.Parse(c.Element("LastModified")?.Value),
				Size = int.Parse(c.Element("Size")?.Value),
				ETag = c.Element("ETag")?.Value,
				StorageClass = c.Element("StorageClass")?.Value
			};
			result.Add(entry);
		}
		
		return result;
	}
	
	private string _xml;

	[Params(10, 100, 1000, 10000, 100000)]
	public int EntryCount;
	
	[GlobalSetup]
	public void Setup()
	{
		var sb = new StringBuilder();
		for(int i = 0; i < EntryCount; i++)
		{
			sb.Append(contents);
		}
		
		_xml = xml.Replace("@c", sb.ToString());
	}

	[Benchmark]
	public IReadOnlyCollection<IOEntry> XmlReader()
	{
		return ParseWithXmlReader(_xml, out _);
	}
	
	[Benchmark]
	public IReadOnlyCollection<IOEntry> Linq()
	{
		return ParseWithXElement(_xml, out _);
	}

}


To contact me, send an email anytime or leave a comment below.