Introduction and Motivation
The motivation for this work stems in part from Cython, a superset of Python that enables developers to achieve performance closer to that of C while retaining much of the Python programming model CYTHON.
The performance gains of Cython are achieved through several mechanisms. Firstly, Cython allows variables, function parameters and return values to be declared using static C types, reducing the overhead associated with Python's dynamic type system. Secondly, statically typed values can be compiled directly in C, avoiding runtime type checks and object allocations. Thirdly, Cython can invoke C functions and libraries directly, bypassing the Python interpreter for computationally intensive tasks. The generated code remains interoperable with the Python runtime and object model.
The final point mentioned above is particularly relevant to this work because it parallels the way SaxonC integrates Python extension functions with XSLT, XQuery, and XPath (Details on the Saxon variants, see Appendix A). In both cases, computationally intensive operations can be delegated to Python code while remaining accessible from a higher-level programming environment.
There are other tools that mix host language functionity too. For example, the XForms implementation Saxon-Forms SAXONFORMS was built using SaxonJS to run in web browsers. The extensibility of Saxon-Forms is important for certain applications, where some XForms functions not in the XPath specification are implemented as user-defined functions written directly in JavaScript. The paper also describes the use of application logic implemented in JavaScript to meet the needs of its users. These functions can then be imported and invoked from XPath expressions used within XForms applications.
This raises an interesting question for XML technologies: can host-language extension functions provide similar benefits for XSLT, XQuery, and XPath applications, allowing developers to preserve a declarative XML-processing architecture while selectively benefiting from the performance characteristics of native languages?
The experimental analysis presented in this paper focuses on SaxonJ and SaxonC (specifically the commercial products - named as EE). SaxonJ is implemented entirely in Java and is designed to run on a traditional JVM. SaxonC, by contrast, is built using GraalVM Native Image technology (AOT compiler) GRAALVM, a JVM implementation capable of compiling Java applications ahead of time into native code and packaging them as shared libraries.
Effectively, a lightweight VM runtime executes within the native library, allowing native applications to invoke functionality implemented in Java. This enables applications written in other languages, such as in C and C++, to call Saxon without requiring a separate Java installation.
The resulting binary includes the compiled SaxonJ code together with the required JDK libraries and runtime components needed to execute the native image. Creating a native image offers several advantages, including a C/C++ API, faster startup times, lower resource usage, greater flexibility when integrating with other programming languages, and simplified deployment and packaging. In addition to its C and C++ APIs, SaxonC provides language bindings for Python and PHP.
Before presenting our experimental analysis and results, we first describe how extension functions are implemented in Saxon. The discussion focuses on SaxonJ for Java and SaxonC, which adopts a different approach to integrating and supporting host languages such as C++, Python, and PHP. We then detail our experimental design, performance analysis and discussion of results then finally conclusion on findings.
User Extension Function Implementations
Extension functions allow code written in a host programming language (for example: Java, C++, Python or PHP) to be invoked directly from a function call in an XPath expression (whether in XSLT, XQuery, or pure XPath). For this to work the Saxon processor needs to be able to locate the executable code corresponding to a particular function name. The function arguments need also to be available and are passed as either XDM types or converted to the host language type system, with the opposite conversion applied to the return value of the function.
Why extension functions?
Before describing the various user extension function implementations,[1] it is worth considering why extension functions are useful and why a developer might choose to use them. One reason is that modern XML processors, such as Saxon or BaseX, provide mechanisms for extending XSLT, XQuery, and XPath with functions written in a host programming language. This flexibility allows XML technologies to be integrated more easily into larger software systems and enables developers to reuse existing libraries and functionality written in languages such as Java, C++, Python, and PHP.
Another motivation is performance. Certain operations are more naturally expressed and executed in imperative programming languages than in XSLT or XPath. For example, computationally intensive algorithms, numerical calculations, bitwise operations, or calls to specialised native libraries may incur significant overhead when implemented purely in XSLT or XPath. In such cases, implementing the computationally intensive portion of the workload as an extension function can provide substantial performance benefits while still allowing the overall application logic to be expressed in XSLT, XQuery, or XPath.
Implementation in SaxonJ (Java)
In SaxonJ at a high level, the implementation works as follows
SAXONJ-EX: The user creates a Java class that extends
ExtensionFunctionDefinition class:[2]
-
Class declares:
-
The function name (
StructuredQName) -
Argument types
-
Result type
-
Cardinality
-
-
The user then has to register the function with the Saxon processor configuration.
-
When a call on the extension function is encountered during XPath, XSLT, or XQuery execution, Saxon invokes the corresponding
ExtensionFunctionCallobject. -
Arguments to the call function are passed as an array of objects of the XDM data model,[3] and the return type must also be an XDM value.
Implementation in SaxonC (C++/Python/PHP)
The implementation of user-defined extension functions in SaxonC builds upon the SaxonJ implementation. It extends the ExtensionFunctionDefinition class; however, to support functions written across language boundaries for C++, Python, and PHP, the underlying implementation differs significantly SAXONC-EX.
The user function registration is done through the SaxonProcessor using a JSON
description that specifies the function namespace, name, function arity, argument
types, and return type.
The host-language function is supplied to Saxon as a function pointer. When an XPath, XSLT, or XQuery executable invokes the extension function, GraalVM is used to call the user-supplied function through a thin C layer, which dispatches the callback to the corresponding C++, Python, or PHP implementation. During this process, the function arguments are converted to the appropriate host-language primitive type system, if possible. For the XDM tree node types these cannot be converted to a primitive type therefore we leave these as wrapper objects in C++, Python or PHP. The return value is converted back into an XDM type so that it can be consumed by the Saxon processing engine.
See example below which shows in C++ the setup of a user function in XSLT. Its a
similar approach in Python and PHP. But in Python the user has the additional option
to request conversion of XDM types to primitive python types where possible. Also
in
Python the method argument has a keyword argument to get access to the
PySaxonProcessor object used for the configuration.
This method illustrates a CPU intensive task for calculating the square root of a set of numbers iterated a certain number of times. The arguments match those defined in the signature (see below) and are passed in the XSLT/XQuery/XPath call:
XdmValue * calculate(int argc, XdmValue ** argv) {
long iterations =
((XdmAtomicValue *)argv[0]->getHead())->getLongValue();
double otherData =
((XdmAtomicValue *)argv[1]->getHead())->getDoubleValue();
double result = 0.0;
for (long i = 1; i <= iterations; i++) {
result += std::sqrt(static_cast<double>(i)) + otherData;
}
return processor->makeDoubleValue(result);
}
The registration of the calculate user function is given below.
Notice the pointer to the calculate function is passed to the SaxonProcessor which
is used in the round-tripping to call the function when required in the XSLT, XPath
or in XQuery:
proc->registerUserFunction("www.host-lang-func.com", &calculate,
"{ \"calculate\": {"
" \"as\": \"xs:double\","
" \"param\": ["
" \"xs:integer\", "
" \"xs:integer\""
" ],"
" \"arity\": [2]"
" }"
"}");
The XSLT code that calls the C++ calculate function is shown below.[4] The namespace used to register the function must match the namespace declared in the XSLT; otherwise, the function will not be found. For example:
xmlns:func="www.host-lang-func.com"
The
namespace URI specified during function registration must be identical to the URI
used in the XSLT declaration.
<xsl:template name="funcCall">
<results>
<!-- Repeat the expensive function 100 times -->
<xsl:for-each select="1 to $rpt">
<run number="{.}">
<xsl:value-of select="func:calculate($input, .)"/>
</run>
</xsl:for-each>
</results>
</xsl:template>
Experimental anaylsis
In this section, we present a number of examples. For each of them we compare the
execution time performance of a pure XSLT stylesheet that uses the
xsl:function instruction to perform a task with an equivalent modified
XSLT stylesheet that perform the same task using a user-defined extension function.
To
broaden the scope of the experiments, we execute the stylesheets in Java, C++, and
Python implementations. This is achieved through the SaxonJ s9api and SaxonC APIs,
with
equivalent user extension functions implemented in each host language.
The experiments are designed to address several questions. Is pure XSLT faster in all cases? Can user extension functions be used to improve performance for certain classes of tasks? What overheads are introduced when invoking host-language functions from XSLT, and under what circumstances should such functions be avoided? By examining both computational and XML-processing workloads, we aim to identify the situations in which extension functions provide the greatest benefit and the trade-offs involved in their use.
Setup
The experiments were conducted using the release version 13.1 for both SaxonJ and SaxonC. At the time of writing, this version was a pre-release. The test machine was an Apple MacBook Pro equipped with an M3 Max processor and 64 GB of memory.
The performance analysis is based on four benchmark scenarios, each implemented
both as a pure XSLT xsl:function and as an equivalent user-defined
extension function written in Java, C++, and Python:
-
math:sqrt(number)— performs square-root calculations over 10 million numbers, repeated 100 times. This benchmark is intended to represent a CPU-intensive computational workload.-
XSLT function:
<xsl:function name="xf:calculate" as="xs:double"> <xsl:param name="iterations" as="xs:integer"/> <xsl:param name="context" as="xs:integer"/> <xsl:sequence select=" sum( for $i in 1 to $iterations return math:sqrt($i) + $context ) "/> </xsl:function>> -
The C++ function:
XdmValue * calculate(int argc, XdmValue ** argv) { long iterations = ((XdmAtomicValue *)argv[0]->getHead())->getLongValue(); double context = ((XdmAtomicValue *)argv[1]->getHead())->getDoubleValue(); double result = 0.0; for (long i = 1; i <= iterations; i++) { result += std::sqrt(static_cast<double>(i)) + context; } return processor->makeDoubleValue(result); }The Java code is similar:
public XdmValue call(XdmValue[] arguments) throws SaxonApiException { long iterations = ((XdmAtomicValue) arguments[0].itemAt(0)) .getLongValue(); long context = ((XdmAtomicValue) arguments[1].itemAt(0)) .getLongValue(); double total = 0.0; for (int i = 1; i <= iterations; i++) { total += Math.sqrt(i) + context; } return new XdmAtomicValue(total); }The equivalent Python code is given below. For the Python extension we do the following:
def calculate(iterations, context, **kwargs): result = sum((math.sqrt(i) + context) for i in range(1, iterations + 1)) return result
-
-
abs(number)— performs a simple absolute-value calculation repeated 100,000 times. This benchmark is intended to measure the overhead associated with calling an extension function for a relatively inexpensive operation.-
XSLT function:
<xsl:function name="f:calculate" as="xs:double"> <xsl:param name="iterations" as="xs:integer"/> <xsl:sequence select="abs($value)"/> </xsl:function> -
Java function:
new XdmAtomicValue(Math.abs((double)value)). Similarly in C++ this would beprocessor->makeDoubleValue(std::abs((double)value))
-
-
Tree navigation using a predicate on a 10 MB XMark XML document. The
$personvariable is given as a string parameter to the stylesheet.-
XSLT function:
$node//person[name = $person] -
In Java, C++ and Python we would need to traverse the tree using the XDM node API and look for the person with "name" element that matches a certain string name.
-
-
Tree navigation and string-length computation over all nodes in a 100 KB XMark XML document.
-
XSLT function:
<xsl:function name="xf:process-node" as="xs:integer"> <xsl:param name="node" as="node()"/> <xsl:param name="iterations" as="xs:integer"/> <xsl:sequence select=" sum( for $i in 1 to $iterations return sum( for $n in $node/descendant-or-self::node() return string-length(string($n))+$i ) ) "/> </xsl:function> -
In Java, C++ and Python we can use a recursive function to get the string-lengths on the XDM node in the tree.
-
Running Times
The following table shows the execution times, measured in seconds.
Table I
Benchmark Results
| SaxonJ - XSLT (Java) | SaxonJ-XSLT (Java+Func) | SaxonC-XSLT (Python) | SaxonC-XSLT (Python+Func) | SaxonC-XSLT (C++) | SaxonC-XSLT (C++ +Func) | |
|---|---|---|---|---|---|---|
| math:sqrt() and sum calculation | 9s | 0.63s | 71s | 55s | 71s | 0.76s |
| abs() | 0.052s | 0.066s | 0.08s | 0.23s | 0.086s | 0.21s |
| Tree-walk and string length calc | 7.2s | 7.6s | 28.1s | 77.8s | 23s | 202s |
| Tree-walk with predicate | 0.022s | 0.042s | 0.017s | 0.342s | 2.59s | 2.64s |
Discussion
It is worth mentioning again that SaxonC is built from SaxonJ using GraalVM native-image technology to produce native code. Consequently, we expect some overhead when invoking callbacks in SaxonC, as the C++ and Python APIs must ultimately interact with the underlying Saxon implementation. However, GraalVM is highly optimised and can, in some cases, deliver faster execution times due to reduced startup overhead, the absence of JVM warm-up costs, and the ability to execute operations at near-native speed.
Comparison of the execution times across the different benchmark scenarios reveals several interesting findings, which we summarise as follows:
-
When is it advisable to use user extension functions? The
math:sqrt()benchmark test is a clear example where the use of user extension functions across all host languages delivered significantly better performance than running pure XSLT in the same environment. In the case of Java and C++, the extension-function implementations achieved performance close to optimal native execution speed. As a baseline running themath:sqrt()in pure Java code that is without any XSLT, ran in 0.673 seconds. The pure XSLT implementation in Java completed in approximately 9 seconds, whereas the version using an extension function completed in just over 1 second, making it approximately seven times faster.We observed similar execution times for the C++ extension function, which also completed in just over 1 second. In this benchmark, most of the work was performed within the C++ function itself. By contrast, the pure XSLT implementation executed through SaxonC C++ API ran in approximately 71 seconds to completion. This indicates that, for this CPU-intensive workload, pure XSLT execution in SaxonC was almost eight times slower than pure XSLT execution in Java. In general, we observed here the overhead associated with crossing the boundary between the SaxonC APIs and the underlying implementation was approximately four times greater than in Java, although this varied depending on the nature and duration of the workload.
The SaxonC Python implementation showed similar times for the pure XSLT execution to those observed in C++. The Python extension function was completed in approximately 55 seconds. This is still faster than pure XSLT executed in Python. But this raises the question of why the square-root benchmark performs substantially faster in C++ than in Python. The difference can largely be explained by the overhead of repeated execution within the Python interpreter, including dynamic type handling, function invocation, and object management. By contrast, the C++ implementation is compiled directly to machine code, allowing the computation to execute much more efficiently.
-
Tree navigation is a clear winner for pure XSLT. Across all languages, the tree-navigation benchmark using a predicate showed that pure XSLT significantly outperformed the equivalent extension-function implementation. In Saxon-EE, a query such as
//x[a=42]is optimized by building a document-level index, meaning that if such a query is executed multiple times, all but the first execution operate in constant time, independent of document size. The user-written extension function does not attempt any similar optimization. On Java and Python, pure XSLT performed better than user extension functions: 2.5 times faster for Java and 15 times faster for Python. The results for C++ were much closer between pure XSLT and C++ user function, which may be due to inefficiencies in the allocation and deallocation of objects created during tree navigation. Each callback also requires crossing the langauge boundary between the host-language API and the underlying Saxon implementation in Java.Another explanation is for Java and Python, the objects lifetime are largely managed by the garbage collection, the additional memory-management overhead in C++ may contribute to the observed slowdown. The tree-walk benchmark involving string retrieval also showed that pure XSLT execution in SaxonC was approximately four times slower than the corresponding Java execution. Unlike the pure Java implementation, SaxonC incurs additional overhead because string values must be copied when crossing the boundary between the underlying Java implementation and the C++ API layer.
-
For simple operations pure XSLT versus extension functions shows little advantage. The
abs()benchmark showed only a small difference between the pure XSLT implementation and the extension-function implementation in Java. For such a simple operation, the overhead of invoking an extension function outweighs most of the potential benefits. The small difference can be concluded as machine differences at the time of execution. In these cases, pure XSLT is generally preferable because it avoids the additional complexity associated with implementing and maintaining user-defined extension functions.
Conclusion
In summary, the results show that combining XSLT or XQuery with host-language user-defined extension functions can significantly improve performance. However, their use should be guided by the nature of the task being performed.
Extension functions should be viewed as a powerful tool for addressing computationally intensive operations or tasks that are difficult to express efficiently in XSLT or XQuery alone. In some cases, the performance gains may arise from limitations or inefficiencies in the implementation of a particular XSLT or XQuery solution. Where there is a substantial performance gap, as demonstrated by the math square root benchmark and other computationally intensive or recursive operations, the use of a host-language extension function can provide dramatic improvements in execution time.
By contrast, for XML-centric operations such as tree navigation and predicate evaluation, pure XSLT often remains the most efficient solution. This is particularly true when the XPath optimizer finds an smart execution strategy, for example, construction of an index as in this example.Developers should therefore consider extension functions selectively, using them where they provide a clear benefit while continuing to rely on the strengths of the XSLT and XQuery processing model for document-oriented tasks.
Appendix A. Overview of Saxon Products
This appendix provides a brief overview of the Saxon product family to give context for the experiments presented in this paper. Saxon is an implementation of the W3C Recommendations for XSLT, XQuery, XPath, and XML Schema validation, and forms part of a wider ecosystem of XML technologies used for transformation, querying, validation, and document processing SAXON.
Saxon is available in several product variants, each designed to support different deployment environments and application requirements. The user-defined extension function feature described in this paper is available in the commercial edition of Saxon, called Saxon-EE.
SaxonJ
SaxonJ is the Java implementation of Saxon and is the reference implementation from which the other Saxon products are derived. It provides APIs through the s9api interface and supports execution of XSLT, XQuery, XPath, and XML Schema validation on the Java Virtual Machine (JVM). SaxonJ also provides the extension function framework used to implement user-defined functions in Java.
SaxonC
SaxonC is built from SaxonJ using GraalVM Native Image technology to produce native executables and libraries. It provides native APIs for C and C++, together with language bindings for Python and PHP. Beginning with SaxonC 13, applications can implement user-defined extension functions directly in C++, Python, and PHP, allowing XSLT, XQuery, and XPath expressions to invoke host-language code through the SaxonC APIs.
SaxonJS
SaxonJS is the JavaScript implementation of Saxon designed for execution within web browsers and JavaScript environments. It supports client-side execution of XSLT 3.0 stylesheets and forms the execution engine for applications such as Saxon-Forms.
References
[XSLT30] Kay, Michael. XSL Transformations (XSLT) Version 3.0, 8 June 2017. W3C Recommendation. http://www.w3.org/TR/xslt30
[XQUERY31] Robie, Jonathan, Dyck, Michael, and Spiegel, Josh. XQuery 3.1: An XML Query Language, 21 March 2017. W3C Recommendation. https://www.w3.org/TR/xquery-31
[CYTHON] Cython. https://cython.readthedocs.io/
[SAXONFORMS] Delpratt, O'Neil and Lockett, Debbie.
Implementing XForms using interactive XSLT 3.0.
Presented at XML
Prague 2018. In Proceedings of XML Prague 2018: http://archive.xmlprague.cz/2018/files/xmlprague-2018-proceedings.pdf
[SAXON] SaxonJ and SaxonC Products. Saxonica. https://www.saxonica.com/
[SAXONJ-EX] SaxonJ - Java extension functions: simple interface. Saxonica. https://www.saxonica.com/documentation13/index.html#!extensibility/extension-functions-J/ext-simple-J
[SAXONC-EX] SaxonC - C++ and Python Extension functions. Saxonica. https://www.saxonica.com/saxon-c/documentation13/index.html#!extensibility
[BASEX] BaseX Java Bindings. BaseX. https://docs.basex.org/main/Java_Bindings
[GRAALVM] GRaalVM. https://www.Graalvm.org/
[1] XQuery calls them external functions, XSLT extension functions. There is no technical difference.
[2] Reflexive extension functions are also available but not discussed in this paper.
[3] The XDM (XML Data Model) is the W3C standard data model shared between XPath, XQuery, and XSLT. It is defined for the representation of documents, nodes, atomic values, functions, maps, arrays, and sequences that are manipulated by these languages.
[4] When executing code N times for performance measurement, it's important to ensure that it doesn't get "loop lifted" by the optimizer. That's why the call in this case accesses the loop counter.