XSLT Performance in .NETby Dan Frumin
The Microsoft .NET Framework brings with it many new tools and improvements for developers. Among them is a very rich and powerful set of XML classes that allow the developer to tap into XML and XSLT in their applications. By now, everyone is familiar with XML, the markup language that is the basis for so many other standards. XSLT is a transformation-based formatter. You can use it to convert structured XML documents into some other form of text output -- quite often HTML, though it can also generate regular text, comma-separated output, more XML, and so on. If you haven't used XSLT before, you might want to read the previously published article titled "Five Quick Tips to Using XSLT."
Before the Microsoft .NET Framework was released, Microsoft published the XML SDK, now in version 4.0. The XML SDK is COM-based, and so can be used from any development language, not just Microsoft .NET. Its object model is also a little different than the .NET implementation, and therefore requires a bit of learning to use. But in the end, the XML SDK can do the same things for XSLT that the .NET Framework offers.
Which raises the question: how do these two engines compare to each other in performance? This article will answer that question.
In order to test the performance of the two parsers, I used a standard XML file for storing a catalog of books. A file with only one book looked like this:
<?xml version="1.0"?> <catalog> <book id="bk101"> <author>Gambardella, Matthew</author> <title>XML Developer's Guide</title> <genre>Computer</genre> <price>44.95</price> <publish_date>2000-10-01</publish_date> <description>An in-depth look at creating applications with XML.</description> </book> </catalog>
I chose to use an XML file on the file system, rather than a SQL 2000 XML Query, in order to avoid any of the performance noise that SQL might incur. I then created four different versions of the same XML file, containing one, 20, 100, and 500 books, respectively.
After setting up my XML sources, I put together a number of XSLT transform files. I created four different XSLT transform files, increasing in their complexity of the processing and output. The first file did nothing but output the book ID for every node in the XML. The second file generated an output of all of the book information in HTML table form. For the third and fourth files, I wanted to test some actual processing. The third file I created uses a for-each operation with a select that filters for the book with ID "bk101". For the fourth and last test file, I decide to apply a sort to all of the nodes, based on the author's name.
As an example, here's the complete XSLT text for the sorting test:
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0"> <xsl:output method="html"/> <xsl:template match="/catalog"> <table> <xsl:apply-templates select="book"> <xsl:sort select="author"/> </xsl:apply-templates> </table> </xsl:template> <xsl:template match="book"> <tr> <td><xsl:value-of select="@id"/></td> <td><xsl:value-of select="title"/></td> <td><xsl:value-of select="author"/></td> <td><xsl:value-of select="genre"/></td> <td><xsl:value-of select="price"/></td> <td><xsl:value-of select="publish_date"/></td> <td><xsl:value-of select="description"/></td> </tr> </xsl:template> </xsl:stylesheet>
All that remained was applying each XSLT transform to each XML file and tracking the results.
I developed a simple test harness to time all of the individual combinations of XML and XSLT files. The test harness was written in C# as a command-line application. By default, it looked in a folder for every XML and XSLT file and ran each combination through two test classes, one written using the MSXML SDK and the other using the .NET Framework. The test manager then recorded the results in a CSV file that could later be loaded into Excel for analysis.
Each of the two classes was given a
Test member that accepted two parameters,
one a path to the XML file, the other a path to an XSLT file. Each test
was designed to perform an end-to-end transformation of the XML. Because
MSXML's implementation of XSLT returns a string with the transformed
output, the .NET implementation of the test used the same mechanism to get
Each test was split up into five operations: creating the XML objects, preparing the XML objects for transformation (e.g., loading the file from the file system), creating the XSLT objects, preparing the XSLT objects for the transformation, and actually executing the transform itself. By splitting the end-to-end transformation into these individual operations, we gain a better understanding of the inner workings of the objects.
Individual timings were taken immediately after each of the five operations and summed up over one hundred runs, later to be averaged out. This process allows for a better representation of the time required by each operation.
In order to get accurate time measurements, I wrapped the QueryPerformanceCounter and QueryPerformanceFrequency functions offered by the Win32 API in the KERNEL32.DLL library. These functions offer much better timing resolution than the DateTime class in the .NET Framework.
Here's a snippet of code for wrapping those functions in your own class:
[System.Runtime.InteropServices.DllImport("KERNEL32")] private static extern bool QueryPerformanceCounter(ref long lpPerformanceCount); [System.Runtime.InteropServices.DllImport("KERNEL32")] private static extern bool QueryPerformanceFrequency(ref long lpFrequency);
The tests themselves were run on a P4 1.6GHz machine with 512MB of memory. The box had plenty of CPU and memory resources left at the time the tests were run, so performance should be reasonably steady. The tests were run through the command line, using a release build of the TestManager project.
Actual Code Tested
Counter.Set(); xmlDoc = new XmlDocument(); dCreateXml = Counter.GetMilliseconds(); Counter.Set(); xmlDoc.Load(xmlFile); dPrepXml = Counter.GetMilliseconds(); Counter.Set(); xslt = new XslTransform(); dCreateXslt = Counter.GetMilliseconds(); Counter.Set(); xslt.Load(xsltFile); dPrepXslt = Counter.GetMilliseconds(); Counter.Set(); StringWriter sw = new StringWriter(); xslt.Transform(xmlDoc, null, sw); output = (string) sw.ToString(); dRunXslt = Counter.GetMilliseconds();
Counter.Set(); xmlDoc = new DOMDocument40Class(); dCreateXml = Counter.GetMilliseconds(); Counter.Set(); xmlDoc.load(xmlFile); dPrepXml = Counter.GetMilliseconds() ; Counter.Set(); xsl = new FreeThreadedDOMDocument40Class(); xslt = new XSLTemplate40Class(); dCreateXslt = Counter.GetMilliseconds(); Counter.Set(); xsl.load(xsltFile); xslt.stylesheet = xsl; xslProc = xslt.createProcessor(); xslProc.input = xmlDoc; dPrepXslt = Counter.GetMilliseconds() ; Counter.Set(); xslProc.transform(); output = (string) xslProc.output; dRunXslt = Counter.GetMilliseconds();
As mentioned above, the .NET implementation uses a StringWriter class to access the string output of the transformation. Getting the MSXML SDK implementation to work required a few unique steps. First, the actual XSLT object requires that the XML for the stylesheet be provided in a FreeThreadedDOMDocument object, rather than a regular DOMDocument object. Second, an IXSLProcessor object must be used to execute the transform itself. Beyond these little details, the code is self-explanatory.
Results and Observations
|Figure 1. Results for Sorting XSLT|
A few items jump out after analyzing the numbers generated by the tests, as illustrated using the sorting XSLT test.
- COM/Interop overhead is significant. Both the creation and deletion of intrinsic .NET objects is much cheaper than creating COM objects, especially since .NET's garbage collector can optimize the operations. As you can see from the left-most column, the CreateXML time increased linearly for the MSXML parser. That's because in our tests, creation actually covers the creation of a new object and the implicit deletion of the old object. Unlike .NET, which simply places its objects on the GC queue, COM objects must be freed. In the case of a very large (500-node) XML file, freeing all of those internal objects is taking quite a while (7 to 8ms).
- The XML parser in MSXML is significantly more efficient than the NET equivalent, especially as the size of the XML file increases. Unfortunately, this benefit is somewhat offset by the cost of creating and deleting the MSXML COM objects. In fact, in smaller XML file sizes (20 nodes), the combined cost of those two operations was actually greater than the combined cost of the .NET implementation's equivalent.
- The extra IXSLProcessor object incurs around a 1ms overhead. The MSXML objects consistently took an extra 1ms in preparing the XSLT for transformation. That can easily be attributed to the extra object creation and passing. This particular operation involves loading an XML file for the stylesheet. As the stylesheet grows in size, the MSXML parser should catch up to the .NET implementation. In addition, a quick scan of the graph shows that this 1ms is negligible compared to the cost of the rest of the operation, especially for larger source files. Nonetheless, developers should optimize this out if possible.
- The MSXML processor is consistently three to four times faster in larger and more complex transformations. This is the most important result, and most significant difference. The MSXML parser transformed the 100-node file in 3.0 to 3.5ms, depending on the complexity of the operation, with sorting being the most complex. By comparison, the .NET implementation required 11 to 13ms to execute the same transformation. The ratio of time (around 3x) appeared consistently up to the 500-node file, dropping only in the simplest case of a one-node XML file.
Looking at the results, we can see that in a single end-to-end operation, the cost of the COM overhead can offset the advantages gained in transformation. This is especially true for smaller XML files (20 to 40 nodes). However, the margin of difference grows as the input files grow in size and as the transformation grows in complexity. When dealing with these scenarios, developers should consider using MSXML as well as two techniques to optimize their applications.
First, consider storing the XSLT transform objects (including IXSLProcessor) in some shared location (e.g., a static member) for future use. This eliminates the cost of creating and preparing the XSLT objects and allows for a reusable transformation object that can simply be applied to XML input.
Second, developers should consider creating their own COM object garbage collector for the XML files, especially if they are large in size. The assumption is that the XSLT transform won't change often, but the input files will (e.g., through data changes as a result of order entry.) Clearly, the creation time of the COM object itself is constant regardless of input file size. That means that most of the cost we see in the CreateXML step is actually part of the deletion of the COM object. After using an XML object, developers could place it into a simple queue and use a separate thread to free those objects. This eliminates another big chunk of time from the operation.
A combination of these techniques and the MSXML objects could easily shave 60 to 70% of the time involved in such a transformation. This level of savings will directly translate into faster performance of the application, as well as greater scalability.
Dan Frumin is a long-time technology executive, with over 10 years of experience in the industry.
Return to ONDotnet.com