Optimizing XPath

Overview

This document outlines optimizations that we can perform to execute xpath-expressions faster.

Stage 1, DONE

Summary

Speed up retrieval of orderInfo objects by storing them in resp. node instead of in a hash.

Details

We currently spend a GREAT deal of time looking through a DOMHelper::orders hash looking for the orderInfo object for a specific node. If we moved the ownership and retrieval of these orderInfo objects to the Node class instead we will probably save a lot of time. I.E. instead of calling myDOMHelper->getDocumentOrder(node) you call node->getDocumentOrder() which then returns the orderInfo object.

It would also be nice if we at the same time fixed some bugs wrt the orderInfo objects and the function that sorts nodes using them.

Bugs filed at this are 88964 and 94471

Stage 2, DONE

Summary

Speed up document-order sorting by having the XPath engine always return document-ordered nodesets.

Details

Currently the nodesets returned from the XPath engine are totally unordered (or rather, have undefined order) which forces the XSLT code to sort the nodesets. This is quite expensive since it requires us to generate orderInfo objects for every node. Considering that many XPath classes actually returns nodesets that are already ordered in document order (or reversed document order) this seems a bit unnecessary.

However we still need to handle the classes that don't by default return document-ordered nodesets. A good example of this is the id() function. For example "id('foo bar')" produces two nodes which the id-function has no idea how they relate in terms of document order. Another example is "foo | bar", where the UnionExpr object gets two nodesets (ordered in document order since all XPath classes should now return ordered nodesets) and need to merge them into a single ordered nodeset.

Stage 3, DONE

Summary

Refcount ExprResults to reduce the number of objects created during evaluation.

Details

Right now every subexpression creates a new object during evaluation. If we refcounted objects we would be often be able to reuse the same objects across multiple evaluations. We should also keep global result-objects for true and false, that way expressions that return bool-values would never have to create any objects.

This does however require that the returned objects arn't modified since they might be used elsewhere. This is not a big problem in the current code where we pretty much only modify nodesets in a couple of places.

To be able to reuse objects across subexpressions we chould have an ExprResult::ensureModifyable-function. This would return the same object if the refcount is 1, and create a new object to return otherwise. This is especially usefull for nodesets which would be mostly used by a single object at a time. But it could be just as usefull for other types, though then we might need a ExprResult::ensureModifyableOfType(ExprResult::ResultType)-function that only returned itself if it has a refcount of 1 and is of the requsted type.

Stage 4

Summary

Speed up evaluation of XPath expressions by using specialized classes for common optimizable expressions.

Details

Some common expressions are possible to execute faster if we have classes that are specialized for them. For example the expression "@foo" can be evaluated by simply calling |context->getAttributeNode ("foo")|, instead we now walk all attributes of the context node and filter each node using a AttributeExpr. Below is a list of expressions that I can think of that are optimizable, but there are probably more.

One thing that we IMHO should keep in mind is to only put effort on optimising expressions that are actually used in realworld stylesheets. For example "foo | foo", "foo | bar[0]" and "foo[position()]" can all be optimised to "foo", but since noone should be so stupid as to write such an expression we shouldn't spend time or codesize on that. Of course we should return the correct result according to spec for those expressions, we just shouldn't bother with evaluating them fast.

Apart from finding expression that we can evaluate more cleverly there is also the problem of how and where do we create these optimised objects instead of the unoptimised, general ones we create now. And what are these optimised classes, should they be normal Expr classes or should they be something else? We could also add "optional" methods to Expr which have default implementations in Expr, for example a ::isContextSensitive() which returns MB_TRUE unless overridden. However we probably can't answer all this until we know which expressions we want to optimised and how we want to optimise them.

These expressions can be optimised:

Use case 1

Class:

Steps along the attribute axis which doesn't contain wildcards

Example:

@foo

What we do today:

Walk through the attributes NamedNodeMap and filter each node using a NameTest.

What we could do:

Call getAttributeNode (or actually getAttributeNodeNS) on the contextnode and return a nodeset containing just the returned node, or an empty nodeset if NULL is returned.

Use case 2

Class:

Union expressions where each expression consists of a LocationStep and all LocationSteps have the same axis. None of the LocationSteps have any predicates (well, this could be relaxed a bit)

Example:

foo | bar | baz

What we do today:

Evaluate each LocationStep separately and thus walk the same path through the document each time. During the walking the NodeTest is applied to filter out the correct nodes. The resulting nodesets are then merged and thus we generate orderInfo objects for most nodes.

What we could do:

Have just one LocationStep object which contains a NodeTest that is a "UnionNodeTest" which contains a list of NodeTests. The UnionNodeTest then tests each NodeTest until it finds one that returns true. If none do then false is returned. This results in just one walk along the axis and no need to generate any orderInfo objects.

Use case 3

Class:

Steps where the predicates isn't context-node-list sensitive.

Example:

foo[@bar]

What we do today:

Build a nodeset of all nodes that match 'foo' and then filter the nodeset through the predicate and thus do some node shuffling.

What we could do:

Create a "PredicatedNodeTest" that contains a NodeTest and a list of predicates. The PredicatedNodeTest returns true if both the NodeTest returns true and all predicats evaluate to true. Then let the LocationStep have that PredicateNodeTest as NodeTest and no predicates. This will save us the predicate filtering and thus some node shuffling. (Note how this combines nicely with the previous optimisation...) (Actually this can be done even if some predicates are context-list sensitive, but only up until the first that isn't.)

Use case 4

Class:

PathExprs that only contains steps that from the child:: and attribute:: axes.

Example:

foo/bar/baz

What we do today:

For each step we evaluate the step once for every node in a nodeset (for example for the second step the nodeset is the list of all "foo" children) and then merge the resulting nodesets while making sure that we keep the nodes in document order (and thus generate orderInfo objects).

What we could do:

The same thing except that we don't merge the resulting nodeset, but rather just concatenate them. We always know that the resulting nodesets are after each other in node order.

Use case 5

Class:

List of predicates where some predicate are not context-list sensitive

Example:

foo[position() > 3][@bar][.//baz][position() > size() div 2][.//@fud]

What we do today:

Apply each predicate separately requiring us to shuffle nodes five times in the above example.

What we could do:

Merge all predicates that are not node context-list sensitive into the previous predicate. The above predicate list could be merged into the following predicate list foo[(position() > 3) and (@bar) and (.//baz)][(position() > size() div 2) and (.//@fud)] Which only requires two node-shuffles

Use case 6

Class:

Predicates that are only context-list-position sensitive and not context-list-size sensitive

Example:

foo[position() > 5][position() mod 2]

What we do today:

Build the entire list of nodes that matches "foo" and then apply the predicates

What we could do:

Apply the predicates during the initial build of the first nodeset. We would have to keep track of how many nodes has passed each and somehow override the code that calculates the context-list-position.

Use case 7

Class:

Predicates that are constants

Example:

foo[5]

What we do today:

Perform the appropriate walk and build the entire nodeset. Then apply the predicate.

What we could do:

There are three types of constant results; 1) Numerical values 2) Results with a true boolean-value 3) Results with a false boolean value. In the case of 1) we should only step up until the n:th node (5 in above example) and then stop. For 2) we should completely ignore the predicate and for 3) we should return an empty nodeset without doing any walking. In some cases we can't at parsetime decide if a constant expression will return a numerical or not, for example for "foo[$pos]", so the decision of 1) 2) or 3) would have to be made at evaltime. However we should be able to decide if it's a constant or not at parsetime. Note that while evaluating a LocationStep [//foo] can be considered constant.

Use case 8

Class:

PathExprs that contains '//' followed by an unpredicated child-step.

Example:

.//bar

What we do today:

We walk the entire subtree below the contextnode and at every node we evaluate the 'bar'-expression which walks all the children of the contextnode. This means that we'll walk the entire subtree twice.

What we could do:

Change the expression into "./descendant::bar". This means that we'll only walk the tree once. This can only be done if there are no predicates since the context-node-list will be different for predicates in the new expression. Note that this combines nicely with the "Steps where the predicates isn't context-node-list sensitive" optimization.

Use case 9

Class:

PathExprs where the first step is '.'

Example:

./*

What we do today:

Evaluate the step "." which always returns the same node and then evaluate the rest of the PathExpr.

What we could do:

Remove the '.'-step and simply evaluate the other steps. In the example we could even remove the entire PathExpr-object and replace it with a single Step-object.

Use case 10

Class:

Steps along the attribute axis which doesn't contain wildcards and we only care about the boolean value.

Example:

foo[@bar], @foo or @bar

What we do today:

Evaluate the step and create a nodeset. Then get the bool-value of the nodeset by checking if the nodeset contain any nodes.

What we could do:

Simply check if the current element has an attribute of the requested name and return a bool-result.

Use case 11

Class:

Unpredicated steps where we only care about the boolean value.

Example:

foo[processing-instruction()]

What we do today:

Evaluate the step and create a nodeset. Then get the bool-value of the nodeset by checking if the nodeset contain any nodes.

What we could do:

Walk along the axis until we find a node that matches the nodetest. If one is found we can stop the walking and return a true bool-result immediatly, otherwise a false bool-result is returned. It might not be worth implementing all axes unless we can reuse code from the normal Step-code. This could also be applied to PathExprs by getting the boolvalue of the last step.

Use case 12

Class:

Unpredicated steps where we only care about the string-value.

Example:

starts-with(processing-instruction(), 'hello')

What we do today:

Evaluate the step and create a nodeset. Then get the string-value of the nodeset by getting the stringvalue of the first node.

What we could do:

Walk along the axis until we find a node that matches the nodetest. If one is found we can stop the walking and return a string-result containing the value of that node. Otherwise an empty string-result can be returned.
This can also be done when we only care about the number-value.
This could be combined with the "Unpredicated steps where we only care about the boolean value" optimization by instead of returning a bool-value or string-value return a nodeset containing just the found node. If that is done this optimization could be applied to PathExprs.

Use case 13

Class:

Expressions where the value of an attribute is compared to a literal.

Example:

@bar = 'value'

What we do today:

Evaluate the attribute-step and then compare the resulting nodeset to the value.

What we could do:

Get the attribute-value for the element and compare that directly to the value. In the above example we would just call getAttr('bar', kNameSpaceID_None) and compare the resulting string with 'value'.

Use case 14

Class:

PathExprs where the last step has a predicate that is not context-nodeset dependent and that contains a part that is not context-node dependent.

Example:

foo/*[@bar = current()/@bar]

What we do today:

What we could do:

First evaluate "foo/*" and "current()/@bar". Then replace "current()/@bar" with a literal (and possibly optimize) and filter all nodes in the nodeset from "foo/*".

Use case 15

Class:

local-name() or namespace-uri() compared to a literal

Example:

local-name() = 'foo'

What we do today:

evaluate the local-name function and compare the string-result to the string-result of the literal.

What we could do:

Atomize the literal (or get the namespaceID in case of namespace-uri()) and then compare that to the atom-name of the contextnode. This is primarily usefull when combined with the previous class.

Use case 16

Class:

Comparisons where one side is a nodeset and the other is not a bool-value.

Example:

//myElem = @baz

What we do today:

Evaluate both sides and then compare them according to the spec.

What we could do:

First of all we should start by evaluating the nodeset-side, if the result is an empty nodeset false can be returned immediatly. Otherwise we evaluate as normal. When both sides are nodesets we should examine them and try to figure out which is faster to evaluate. That expression should be evaluated first (probably by making it the left-hand-side expression).

Use case 17

Class:

Comparisons where one side is a PathExpr and the other is a bool-value.

Example:

baz = ($foo > $bar)

What we do today:

Evaluate both sides and then compare them.

What we could do:

Apply the "Steps where we only care about the boolean value"-optimization on the PathExpr-side and then evaluate as usual.

Use case 18

Class:

Subexpressions that will be evaluated more then once where the only change is in context it doesn't depend on

Example:

foo[@bar = sum($var/@bar)]

What we do today:

Reevaluate the subexpression every time we need it and every time get the same result.

What we could do:

We should save the result from the first evaluation and just bring it back the following time we need it. This can be done by inserting an extra expression between the subexpression and its parent, this expression would then first go look in a cache available through the nsIEvalContext, if the value isn't available there the original expression is evaluated and its result is saved in the cache. The cache can be keyed on an integer which is stored in the inserted 'cache-expression'.
The cache itself could be created by another expression that is inserted at the top of the expression. This way that expression works as a boundry-point for the cache and can in theory be inserted anywhere in an expression if needed.

Stage 5

Summary

Detect when we can concatenate nodesets instead of merge them in PathExpr.

Details

Why can we for expressions like "foo/bar/baz" concatenate the resulting nodesets without having to check nodeorder? Because at every step two statements are true:

  1. We iterate a nodeset where no node is an ancestor of another
  2. The LocationStep only returns nodes that are members of the subtree below the context-node

For example; While evaluating the second step in "foo/bar/baz" we iterate a nodelist containing all "foo" children of the original contextnode, i.e. none can be an ancestor of another. And the LocationStep "bar" only returns children of the contextnode.

So, it would be nice if we can detect when this occurs as often as possible. For example the expression "id(foo)/bar/baz" fulfils those requirements if the nodeset returned from contains doesn't contain any ancestors of other nodes in the nodeset, which probably often is the case in real-world stylesheets.

We should perform this check on every step to be able to take advantage of it as often as possible. For example the in expression "id(@boss)/ancestor::team/members" we can't use this optimisation at the second step since the ancestor axis returns nodes that are not members of the contextnodes subtree. However we will probably be able to use the optimisation at the third step since if iterated nodeset contains only one node (and thus can't contain ancestors of it's members).