Two-stage recursive algorithms in XSLT
By Dimitre
Novatchev and Slawomir
Tyszko
Summary
This article presents several examples of two-stage recursive
XSLT algorithms. In each of them the first stage is implemented as
DVC (divide and conquer) recursive template, and the second stage
as simple recursive template. The examples show that such a
combination results in better performance than other solutions,
including classic DVC-only algorithms
Download the source
XSLT is an offspring of DSSSL, which is based on the
Scheme programming language, and, as such, belongs to the
category of so-called declarative programming languages. As
such it is quite different from a procedural language. One
such outstanding difference is that variables can only be
initialised and are immutable. xsl:param and
xsl:variable elements have local scope and can only be
"shadowed" in a "more local" scope, by another variable with the
same name. Among other things, this makes it difficult to implement
iterative computations, very common in procedural languages. In
cases when such computations are necessary in an XSLT
transformation, the only solution is to resort to recursion, by
having a named template call itself recursively. With this
solution, the intermediate result, instead of being accumulated in
one place in memory, is, after each computation step, pushed from
one place to another on the call-stack, corresponding to different
instances of the an xsl:param at different levels of
recursion.
However, this method, in its simplest "handbook form", can have
some essential and unpleasant limitations. When an XSLT processor
is used, which does not handle tail recursion intelligently (does
not implement tail recursion with iteration) it may crash when
processing big xml source documents, due to the call-stack
overflow. Processing time increases much steeper than in a linear
way before the crash-point is reached with the size of the source
xml document increasing.
There are several ways of addressing this problem. One way was
presented in Dimitre Novatchev's snippet:
Avoiding an XSLT Processor crash due to deep recursive
processing. This method (known as the Piez method) makes it
possible to replace recursion by a xsl:for-each loop, but it
can be used only in the simplest case - in iterations, where there
is no passing of data computed in a given step to the next
steps.
Another solution was proposed in Slawomir Tyszko's snippet: Another way of avoiding XSLT processor
crash. It does not relinquish recursion, but reduces its depth,
by using two named recursive templates instead of just one. In this
solution the input is partitioned into "chunks". The first template
recursively computes partial results inside chunks, while the
second template acts as "chunk manager", as it recursively calls
the first template for processing successive chunks, and uses
returned partial results to compute the final result. If, for
instance, the input consists of 10 000 nodes, it may be
partitioned into 100 chunks, each containing 100 nodes. In such a
case, the recursion depth will be reduced from 10 000 to
200 (100+100).
Before this last snippet was published, Dimitre had been
actively promoting DVC (divide and conquer) approach to
deep-recursion problems in XSLT, mostly in his posts to the
xsl-list forum. No wonder then, that he immediately responded to
the Slawomir's snippet with: DVC Algorithms in XSLT, in which he had
pointed out that, in this case, the classic DVC solution would be
much better: simpler, faster, enabling much higher reduction of the
recursion depth (to only 14 instead of 200).
But, after a more detailed examination, Slawomir was able to
prove this was not always the case. While the DVC solution
was unrivalled in case of computation of a single-number result
(large sum of sales*price products examined in those two
snippets), experimentation with another problem (search-and-replace
on a very long string) had shown that, in that case, the "chunk
algorithm", with adequately chosen chunk-size parameter, was
several times faster (see: Re: DVC Algorithms in XSLT).
Theoretical considerations reveal that both DVC and the "chunk"
solution have advantages, as well as limitations. DVC's main
advantage is a very low recursion depth (to log2N),
which is also a substantial factor determining the speed of the
algorithm, while its limitation stems from the inevitable overhead
necessary to implement this method. This overhead affects mostly
the bottom-level (most numerous) steps of recursion. With the
"chunk" solution the picture is reversed: the recursion depth
reduction is not so radical, but the overhead related to the last
recursion steps is small, as those last steps are performed by the
chunk-processing template, which is a simple, regular recursive
template.
Next comes the following question: isn't it possible to somehow
combine the strong points of those two solutions, so that this
combination would exceed the performance of both solutions on
almost any task? Such ideal solution would have lower depth of
recursion than the chunk algorithm, and at the same time would have
lower overhead, than the pure DVC solution. We now know that the
replacement of the whole of the two-template chunk algorithm, by
the classic DVC solution, was too radical a move; there are
situations where the chunk algorithm is distinctly faster. But what
if we took the chunk algorithm and replaced just the first
recursive template (the chunk manager) with the DVC template,
leaving the second, chunk-processing, template intact? In such a
way we could get a two-stage algorithm; one stage performed by a
classic DVC template, dividing the input into smaller and smaller
segments until the remaining segments are small enough, and the
second stage, performed by a simple recursive template, processing
those small segments (chunks). As compared to the chunk algorithm,
such an algorithm will still have much lower recursion depth. On
the other hand, as compared to pure DVC solution, the overhead,
related to DVC implementation, will be much reduced, since it will
be completely eliminated for the large number of small DVC
divisions, where the overhead is most expressed.
This solution is pursuing a practical goal to maximize the
processing speed, with recursion depth kept well below a safe limit
(e.g. 1000). As it turns out, this practical goal can be achieved.
In fact, last August, in the xsl-list, Dimitre suggested a very
similar solution, as the "optimal" algorithm for reversing strings
(see: http://sources.redhat.com/ml/xsl-list/
2001-08/msg00258.html).
In the rest of the article we investigate three examples of such
two-stage algorithms, and compare them to their classic DVC
equivalents.
This is the example investigated in the above-mentioned
snippets: Another way of avoiding XSLT processor
crash and DVC Algorithms in XSLT. In fact, this
example is based on an example given in chapter 8 (9 in the second
edition) of the Michael Kay's XSLT Programmer's Reference to
demonstrate the use of simple recursion (template named
total-sales-value) for computing the sum of
sales*price products for all book elements contained
in the booklist element:
<booklist>
<book>
<title>Angela's Ashes</title>
<author>Frank McCourt</author>
<publisher>HarperCollins</publisher>
<isbn>0 00 649840 X</isbn>
<price>6.99</price>
<sales>235</sales>
</book>
<book>
<title>Sword of Honour</title>
<author>Evelyn Waugh</author>
<publisher>Penguin Books</publisher>
<isbn>0 14 018967 X</isbn>
<price>12.99</price>
<sales>12</sales>
</book> </booklist>
|
Let us use this task to compare the performance of the simple
recursion, classic DVC recursion, and our new two-stage algorithm.
Below is the recursive solution - implemented by the template
sumSales1:
<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="text"/>
<xsl:template match="/">
<xsl:call-template name="sumSales1">
<xsl:with-param name="pNodes" select="/*/book"/>
</xsl:call-template>
</xsl:template>
<xsl:template name="sumSales1">
<xsl:param name="pNodes" select="/.."/>
<xsl:param name="result" select="0"/>
<xsl:choose>
<xsl:when test="$pNodes">
<xsl:call-template name="sumSales1">
<xsl:with-param name="pNodes"
select="$pNodes[position()!=1]"/>
<xsl:with-param name="result"
select="$result
+ $pNodes[1]/sales*$pNodes[1]/price"/>
</xsl:call-template>
</xsl:when>
<xsl:otherwise>
<xsl:value-of select="$result"/>
</xsl:otherwise>
</xsl:choose>
</xsl:template>
</xsl:stylesheet>
|
And here is the DVC solution:
<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="text" />
<xsl:template match="/">
<xsl:call-template name="sumSales">
<xsl:with-param name="pNodes"
select="/*/book" />
</xsl:call-template>
</xsl:template>
<xsl:template name="sumSales">
<xsl:param name="pNodes" select="/.." />
<xsl:param name="result" select="0" />
<xsl:variable name="vcntNodes"
select="count($pNodes)" />
<xsl:choose>
<xsl:when test="$vcntNodes = 1">
<xsl:value-of select=
"$result + $pNodes/sales * $pNodes/price" />
</xsl:when>
<xsl:otherwise>
<xsl:variable name="vcntHalf"
select="floor($vcntNodes div 2)" />
<xsl:variable name="vValue1">
<xsl:call-template name="sumSales">
<xsl:with-param name="pNodes" select=
"$pNodes[position() <= $vcntHalf]" />
<xsl:with-param name="result"
select="$result" />
</xsl:call-template>
</xsl:variable>
<xsl:call-template name="sumSales">
<xsl:with-param name="pNodes" select=
"$pNodes[position()> $vcntHalf]" />
<xsl:with-param name="result"
select="$vValue1" />
</xsl:call-template>
</xsl:otherwise>
</xsl:choose>
</xsl:template>
</xsl:stylesheet>
|
Finally, the two-stage-algorithm-based solution is a combination
of the previous two:
<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="text" />
<xsl:template match="/">
<xsl:call-template name="sumSales">
<xsl:with-param name="pNodes"
select="/*/book" />
<xsl:with-param name="threshold"
select="10" />
</xsl:call-template>
</xsl:template>
<!-- DVC template: -->
<xsl:template name="sumSales">
<xsl:param name="pNodes" select="/.." />
<xsl:param name="threshold" select="10" />
<xsl:param name="result" select="0" />
<xsl:variable name="vcntNodes"
select="count($pNodes)" />
<xsl:choose>
<xsl:when test="$vcntNodes <= $threshold">
<xsl:call-template name="sumSales1">
<xsl:with-param name="pNodes"
select="$pNodes" />
<xsl:with-param name="result"
select="$result" />
</xsl:call-template>
</xsl:when>
<xsl:otherwise>
<xsl:variable name="vcntHalf"
select="floor($vcntNodes div 2)" />
<xsl:variable name="vValue1">
<xsl:call-template name="sumSales">
<xsl:with-param name="pNodes" select=
"$pNodes[position()<= $vcntHalf]" />
<xsl:with-param name="threshold"
select="$threshold" />
<xsl:with-param name="result"
select="$result" />
</xsl:call-template>
</xsl:variable>
<xsl:call-template name="sumSales">
<xsl:with-param name="pNodes" select=
"$pNodes[position() > $vcntHalf]" />
<xsl:with-param name="threshold"
select="$threshold" />
<xsl:with-param name="result"
select="$vValue1" />
</xsl:call-template>
</xsl:otherwise>
</xsl:choose>
</xsl:template>
<!-- simple recursive template: -->
<xsl:template name="sumSales1">
<xsl:param name="pNodes" select="/.." />
<xsl:param name="result" select="0" />
<xsl:choose>
<xsl:when test="$pNodes">
<xsl:call-template name="sumSales1">
<xsl:with-param name="pNodes"
select="$pNodes[position()!=1]" />
<xsl:with-param name="result" select=
"$result+$pNodes[1]/sales * $pNodes[1]/price"/>
</xsl:call-template>
</xsl:when>
<xsl:otherwise>
<xsl:value-of select="$result" />
</xsl:otherwise>
</xsl:choose>
</xsl:template>
</xsl:stylesheet>
|
The sumSales template first compares the number of nodes,
in the input node-set, with the value of the threshold
parameter. If it exceeds the threshold, the template behaves like a
classic DVC template, i.e. divides the input node-set into two
almost equal parts and calls itself twice (one call with each
part). Otherwise the number of nodes is below the threshold, so the
simple recursive template sumSales1 is called to carry out
the final computations.
To compare the performance of those three stylesheets we used 5
input files: booklist01.xml, booklist02.xml, booklist04.xml,
booklist08.xml and booklist16.xml, generated by
appropriately replicating Michael Kay's original example. They
contained correspondingly: 1000, 2000, 4000, 8000 and 16000
book elements. They are all included in the attached
source.zip file. The tests were carried out with the Xalan
C++ XSLT processor running on the 500 MHZ Pentium, 256 MB RAM PC.
We have compared the speed of all three above algorithms, the last
one using 5 different threshold values.
In the table below are the results in milliseconds, each
obtained by running the corresponding stylesheet 3 times and then
taking the average of the Xalan-reported transformation times:
| Nr of nodes |
Simple recur. |
DVC |
Two-stage algorithm with different thresholds |
| 1 |
2 |
20 |
100 |
200 |
| 1000 |
4230 |
387 |
497 |
383 |
277 |
387 |
607 |
| 2000 |
19990 |
770 |
987 |
710 |
533 |
787 |
1210 |
| 4000 |
111550 |
1760 |
2140 |
1653 |
1173 |
1720 |
2527 |
| 8000 |
- |
3973 |
4833 |
3670 |
2910 |
3957 |
5510 |
| 16000 |
- |
10180 |
11810 |
9667 |
8097 |
10070 |
13217 |
As can be seen, the simple recursive template, in which the
recursion depth equals the number of nodes in the input node-set,
could not cope with the files containing 8000 and 16000 book
elements, and with smaller files the transformation times were many
times higher than with the more sophisticated algorithms. In fact,
with simple recursion, when the recursion depth doubles, the
transformation time increases about 5 times.
The classic DVC template was 11 times faster with the
1000-book-elements file, and 63 times faster with the
4000-elements file, as compared to the simple recursive template.
And it can be seen that, when using DVC, the processing time grows
approximately linearly with the size of the input, i.e. much slower
than with the simple recursion.
The two-stage algorithm, with the threshold parameter set
to 1, is functionally equivalent to the classic DVC solution. It is
slightly slower, though, because in this specific case, in addition
to splitting the input node-set with the DVC template
sumSales, which proceeds in exactly the same way in both
cases, the two-stage algorithm requires also one call to the simple
recursive template sumSales1 for each book element.
However, it is enough to set the threshold to 2 to get this
algorithm overtake pure DVC. The transformation time decreases
until the threshold reaches 10-20, due to DVC overhead
reduction. After that it starts increasing again because of the
growing size of the segments processed by the second-stage
template.
The two-stage algorithm, with optimally set threshold (between
10 and 20), is from 20 to 30% faster than the classic DVC
algorithm. This is not very impressive, although in some real-life
applications such improvement may be significant. The gain is
rather modest because, in this example, the overhead related to the
DVC implementation, as compared to the main processing, is
relatively small, so its reduction, by shifting at some point to
simple recursion, is also not very significant.
The second example is, in fact, an implementation of the idea of
the optimal string-reversal algorithm suggested by Dimitre in http://sources.redhat.com/ml/xsl-list/
2001-08/msg00258.html.
Here is a stylesheet , which implements a classic DVC template
(reverse2). It differs slightly from the template originally
used by Dimitre, the modifications were made in order to reduce the
inherent DVC overhead as much as possible:
<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="text" />
<xsl:template match="/">
<xsl:call-template name="reverse2">
<xsl:with-param name="theString"
select="/*/text()" />
</xsl:call-template>
</xsl:template>
<xsl:template name="reverse2">
<xsl:param name="theString" />
<xsl:variable name="thisLength"
select="string-length($theString)" />
<xsl:choose>
<xsl:when test="$thisLength = 1">
<xsl:value-of select="$theString" />
</xsl:when>
<xsl:otherwise>
<xsl:variable name="length1"
select="floor($thisLength div 2)" />
<xsl:call-template name="reverse2">
<xsl:with-param name="theString"
select="substring($theString,
$length1+1,
$thisLength - $length1
)" />
</xsl:call-template>
<xsl:call-template name="reverse2">
<xsl:with-param name="theString"
select="substring($theString, 1, $length1)" />
</xsl:call-template>
</xsl:otherwise>
</xsl:choose>
</xsl:template>
</xsl:stylesheet>
|
And here is the two-stage version of the string-reversal
stylesheet, built by combining the above code with a simple
recursive template (reverse):
<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="text" />
<xsl:template match="/">
<xsl:call-template name="reverse2">
<xsl:with-param name="theString"
select="/*/text()" />
<xsl:with-param name="threshold"
select="75" />
</xsl:call-template>
</xsl:template>
<!-- DVC template: -->
<xsl:template name="reverse2">
<xsl:param name="theString" />
<xsl:param name="threshold"
select="50" />
<xsl:variable name="thisLength"
select="string-length($theString)"/>
<xsl:choose>
<xsl:when test="$thisLength <= $threshold">
<xsl:call-template name="reverse">
<xsl:with-param name="theString"
select="$theString" />
</xsl:call-template>
</xsl:when>
<xsl:otherwise>
<xsl:variable name="length1"
select="floor($thisLength div 2)"/>
<xsl:call-template name="reverse2">
<xsl:with-param name="theString"
select="substring($theString,
$length1+1,
$thisLength - $length1
)" />
<xsl:with-param name="threshold"
select="$threshold" />
</xsl:call-template>
<xsl:call-template name="reverse2">
<xsl:with-param name="theString"
select="substring($theString, 1, $length1)"/>
<xsl:with-param name="threshold"
select="$threshold" />
</xsl:call-template>
</xsl:otherwise>
</xsl:choose>
</xsl:template>
<!-- simple recursive template: -->
<xsl:template name="reverse">
<xsl:param name="theString" />
<xsl:variable name="thisLength"
select="string-length($theString)"/>
<xsl:choose>
<xsl:when test="$thisLength = 1">
<xsl:value-of select="$theString" />
</xsl:when>
<xsl:otherwise>
<xsl:value-of
select="substring($theString,$thisLength,1)"/>
<xsl:call-template name="reverse">
<xsl:with-param name="theString"
select="substring($theString,
1,
$thisLength -1)" />
</xsl:call-template>
</xsl:otherwise>
</xsl:choose>
</xsl:template>
</xsl:stylesheet>
|
The DVC template (reverse2) first compares the length of
the input string with the threshold parameter. If it exceeds
the threshold, the template behaves like a classic DVC template,
i.e. divides the input string into two equal (almost) segments, and
calls itself twice (one call for each segment). Otherwise, it calls
the simple recursive template reverse.
To compare the performance of the two stylesheets we used 4
input files: reverse01.xml, reverse02.xml,
reverse04.xml and reverse08.xml. Each of them
contained one text node with correspondingly: 100 000,
200 000, 400 000 and 800 000 characters. They are
all included in the attached source.zip file. Like before,
the tests were carried out with the Xalan C++ XSLT processor
running on a 500 MHZ Pentium, 256 MB RAM PC. We have compared the
speed of the two algorithms, the last one using 5 different
threshold values. The stylesheet based on just the simple recursive
template could not have been included in the comparison, since,
even with the smallest input file, it would require a recursion
depth of 100 000, far exceeding Xalan capabilities.
Below are the results in milliseconds, each obtained by running
the corresponding stylesheet 3 times, then taking the average of
the Xalan-reported transformation times:
| String length |
DVC |
Two-stage algorithm with different thresholds |
|
1 |
5 |
75 |
100 |
200 |
| 100000 |
14060 |
19190 |
12027 |
8787 |
9067 |
10233 |
| 200000 |
28140 |
38360 |
24113 |
17613 |
18007 |
20467 |
| 400000 |
56190 |
76787 |
48060 |
35223 |
36250 |
40920 |
| 800000 |
113223 |
154410 |
96413 |
70617 |
72447 |
81963 |
The two-stage algorithm, with the threshold parameter set
to 1, is functionally equivalent to the classic DVC solution. It is
slower, though, because in this specific case, in addition to
splitting the input string with the DVC template, which proceeds in
exactly the same way in both cases, the two-stage algorithm
requires also one call to the simple recursive template
lrReplace3 for each character of the string. However, it is
enough to set the threshold to 5 to get this algorithm
overtake pure DVC. The transformation time decreases until the
threshold reaches approximately 75 characters, due to DVC
overhead reduction. After that, its further reduction becomes less
and less significant, whereas the growing size of the segments
processed by the second-stage template gains more significance,
hence the processing time begins to grow.
The two-stage algorithm, with optimally set threshold (about
75), is about 37% faster than classic DVC solution. This is a more
tangible difference than in the previous example, although still
not very impressive. Nevertheless, it seems sufficiently promising
to encourage us to investigate the last example.
This last example deals with search and replace operations
performed on very long strings. The straightforward solution is, of
course, a simple recursive template (see, for instance, How to do a search and replace in XSLT
or XSLT string Replace function). With
such a solution the recursion depth is equal to the number of
substrings to be replaced, hence, as we already know, it can not be
used to deal with long input strings containing many thousands of
replacements targets.
The next option is the classic DVC solution, which requires
splitting the input string into two possibly equal parts and
applying the recursive template to both parts, and so on until the
parts become very short (e.g. until each of them can not contain
more than one occurrence of the replacement target), and only then
making actual replacements. However, in this case, applying DVC is
not as simple as in the previous examples because, when splitting
the input string, we may also split a target. Such a target will
not be detected in the right part of the input string, or in its
left part, so it will not be replaced.
One way of dealing with this problem (as used in this algorithm)
is to shift a small number of the last characters that may belong
to a target, from the right part to the left part of the input
string. After such a move the target will not be detected in the
right part, but it should be detected in the left part, as now the
left part contains the complete instance of the target.
In order to implement this idea, the recursive template should
not return the whole string passed to it (after accomplishing all
the replacements), but the returned string should be truncated by
an adequate number of characters (leaving out the characters that
could belong to a split target). In addition, the template should
also produce a number specifying how many characters it had left
out. This number (a displacement) is necessary to precisely
determine which part of the input string should be passed to the
next call of the template, so as to include the characters left out
by the previous call. One consequence of this approach is that the
recursive DVC template, applied to the whole input string, will not
process it completely, but some endmost characters may be left out.
Thus, the stylesheet, after calling the template, should extract
the displacement, and copy the indicated number of the endmost
characters from the input string, appending them to the output.
Determining the displacement is very simple: let us denote the
displacement by u (from uncertain), and by s
the remainder of the input string composed of the characters
following the last occurrence of the target. Then when s is
empty, u = 0; when
string-length(s) < string-length(
target),
u = string-length(s); when
string-length(s) >= string-length(
target), u =
string-length(target) - 1.
The DVC template goes on splitting the input string into two
approximately equal segments, until the length of the resulting
segments becomes less than 2*string-length(target),
which means that such a terminal segment can contain at most one
occurrence of the target. If this is the case, instead of further
splitting, the possible replacement takes place, and the template
returns the truncated string along with the displacement.
The last difficulty relates to the fact that our DVC template
must return both the converted string and a number (a
displacement). To make it possible we have had to resort to the
vendor-supplied node-set extension function, so unlike the
previous examples, this algorithm requires an XSLT processor with a
node-set extension implemented. Thus, the output of the DVC
template is stored in a variable, as a result tree fragment: the
converted string - as a text node, and the displacement - as
contents of the element u. After converting the result tree
fragment to a node-set (hence the need of the extension function)
both items can be easily extracted.
Here is the pure DVC solution:
<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:msxsl="urn:schemas-microsoft-com:xslt">
<xsl:output method="text"
encoding="iso-8859-1" />
<xsl:template match="/">
<xsl:variable name="Result">
<xsl:call-template name="lrReplace">
<xsl:with-param name="theString"
select="/*/text()"/>
<xsl:with-param name="target"
select="'AAA'" />
<xsl:with-param name="replacement"
select="'ZZZ'" />
</xsl:call-template>
</xsl:variable>
<xsl:value-of select="$Result" />
</xsl:template>
<xsl:template name="lrReplace">
<xsl:param name="theString"/>
<xsl:param name="target"/>
<xsl:param name="replacement"/>
<xsl:variable name="lStr"
select="string-length($theString)"/>
<xsl:variable name="resRTF">
<xsl:call-template name="lrReplace2">
<xsl:with-param name="theString"
select="$theString"/>
<xsl:with-param name="target"
select="$target"/>
<xsl:with-param name="replacement"
select="$replacement"/>
</xsl:call-template>
</xsl:variable>
<xsl:variable name="resNode-set"
select="msxsl:node-set($resRTF)"/>
<xsl:value-of select="$resNode-set/text()"/>
<xsl:value-of
select="substring($theString,
$lStr -$resNode-set/u+1
)" />
</xsl:template>
<xsl:template name="lrReplace2">
<xsl:param name="theString"/>
<xsl:param name="target"/>
<xsl:param name="replacement" select="''" />
<xsl:variable name="lStr"
select="string-length($theString)" />
<xsl:variable name="lTarget"
select="string-length($target)" />
<xsl:choose>
<xsl:when test="$lStr < $lTarget + $lTarget">
<xsl:choose>
<xsl:when
test="contains($theString,$target)">
<xsl:value-of select=
"substring-before($theString,$target)" />
<xsl:value-of select="$replacement" />
<u>
<xsl:value-of select="string-length
(substring-after($theString,$target))" />
</u>
</xsl:when>
<xsl:otherwise>
<xsl:choose>
<xsl:when test="$lStr >= $lTarget">
<xsl:value-of
select="substring($theString,
1,
$lStr - $lTarget + 1)"/>
<u>
<xsl:value-of select="$lTarget - 1" />
</u>
</xsl:when>
<xsl:otherwise>
<u>
<xsl:value-of select="$lStr" />
</u>
</xsl:otherwise>
</xsl:choose>
</xsl:otherwise>
</xsl:choose>
</xsl:when>
<!-- Now the general case - theString is not less
than twice the replacement -->
<xsl:otherwise>
<xsl:variable name="halfLength"
select="floor($lStr div 2)"/>
<xsl:variable name="processedHalf">
<xsl:call-template name="lrReplace2">
<xsl:with-param name="theString"
select="substring($theString,
1,
$halfLength)"/>
<xsl:with-param name="target"
select="$target"/>
<xsl:with-param name="replacement"
select="$replacement"/>
</xsl:call-template>
</xsl:variable>
<xsl:variable name="nodePrHalf"
select="msxsl:node-set($processedHalf)"/>
<xsl:value-of select="$nodePrHalf/text()"/>
<xsl:call-template name="lrReplace2">
<xsl:with-param name="theString"
select="substring($theString,
$halfLength
- $nodePrHalf/u + 1)" />
<xsl:with-param name="target"
select="$target" />
<xsl:with-param name="replacement"
select="$replacement" />
</xsl:call-template>
</xsl:otherwise>
</xsl:choose>
</xsl:template>
</xsl:stylesheet>
|
The highest-level (in the call-hierarchy) named template in the
above stylesheet is lrReplace, but its role consists only in
calling the DVC template lrReplace2 once, and storing its
output in the variable resRTF as a result tree fragment.
This is necessary because the DVC template may return the string
truncated by several characters. The template lrReplace
first converts the resRTF variable to a node-set (contained
in the variable resNode-set), then extracts from that
node-set, and outputs, the text node containing the truncated
string, and finally outputs the missing part of the string by
copying it from the input string, its length determined by the
displacement extracted from resNode-set as the contents of
the element u.
The DVC template lrReplace2 consists of two parts, the
first part dealing with the input strings shorter than the
concatenation of two targets. That part is a direct implementation
of the rules (given above), specifying how to compute the
displacement. The second part, dealing with longer strings, first
calls itself recursively passing as parameter theString the
first half of the input string, and assigns the resulting result
tree fragment to the variable processedHalf. Then it
converts that result tree fragment to a node-set which becomes the
contents of the variable nodePrHalf. After that, it outputs
the text node extracted from the node-set, and finally, it calls
itself the second time, this time passing, in theString the
second half of the input string preceded by characters left out by
the first call.
As in the previous examples, we compared the performance of this
pure DVC solution with the two-stage algorithm built by combining
the DVC template with the simple recursive one. Here is the XSLT
code of the two-stage algorithm:
<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:msxsl="urn:schemas-microsoft-com:xslt">
<xsl:output method="text"
encoding="iso-8859-1" />
<xsl:template match="/">
<xsl:variable name="Result">
<xsl:call-template name="lrReplace">
<xsl:with-param name="theString"
select="/*/text()"/>
<xsl:with-param name="target"
select="'AAA'"/>
<xsl:with-param name="replacement"
select="'ZZZ'"/>
<xsl:with-param name="threshold"
select="2000"/>
</xsl:call-template>
</xsl:variable>
<xsl:value-of select="$Result" />
</xsl:template>
<xsl:template name="lrReplace">
<xsl:param name="theString"/>
<xsl:param name="target"/>
<xsl:param name="replacement"/>
<xsl:param name="threshold"
select="150"/>
<xsl:variable name="lStr"
select="string-length($theString)"/>
<xsl:variable name="resRTF">
<xsl:call-template name="lrReplace2">
<xsl:with-param name="theString"
select="$theString"/>
<xsl:with-param name="target"
select="$target"/>
<xsl:with-param name="replacement"
select="$replacement"/>
<xsl:with-param name="threshold"
select="$threshold"/>
</xsl:call-template>
</xsl:variable>
<xsl:variable name="resNode-set"
select="msxsl:node-set($resRTF)"/>
<xsl:value-of select="$resNode-set/text()"/>
<xsl:value-of
select="substring($theString,
$lStr - $resNode-set/u+1)"/>
</xsl:template>
<!-- DVC template: -->
<xsl:template name="lrReplace2">
<xsl:param name="theString"/>
<xsl:param name="target"/>
<xsl:param name="replacement"/>
<xsl:param name="threshold" select="150"/>
<xsl:variable name="lStr"
select="string-length($theString)"/>
<xsl:variable name="lTarget"
select="string-length($target)"/>
<xsl:choose>
<xsl:when test="$lStr <= $threshold">
<xsl:call-template name="lrReplace3">
<xsl:with-param name="theString"
select="$theString"/>
<xsl:with-param name="target"
select="$target"/>
<xsl:with-param name="replacement"
select="$replacement"/>
</xsl:call-template>
</xsl:when>
<xsl:otherwise>
<xsl:variable name="halfLength"
select="floor($lStr div 2)"/>
<xsl:variable name="processedHalf">
<xsl:call-template name="lrReplace2">
<xsl:with-param name="theString"
select="substring($theString,
1,
$halfLength)" />
<xsl:with-param name="target"
select="$target" />
<xsl:with-param name="replacement"
select="$replacement"/>
<xsl:with-param name="threshold"
select="$threshold"/>
</xsl:call-template>
</xsl:variable>
<xsl:variable name="nodePrHalf"
select="msxsl:node-set($processedHalf)"/>
<xsl:value-of select="$nodePrHalf/text()"/>
<xsl:call-template name="lrReplace2">
<xsl:with-param name="theString"
select="substring($theString,
$halfLength
- $nodePrHalf/u + 1)"/>
<xsl:with-param name="target"
select="$target"/>
<xsl:with-param name="replacement"
select="$replacement"/>
<xsl:with-param name="threshold"
select="$threshold" />
</xsl:call-template>
</xsl:otherwise>
</xsl:choose>
</xsl:template>
<!-- simple recursive template: -->
<xsl:template name="lrReplace3">
<xsl:param name="theString" />
<xsl:param name="target" />
<xsl:param name="replacement" />
<xsl:choose>
<xsl:when test="contains($theString, $target)">
<xsl:value-of select=
"substring-before($theString, $target)"/>
<xsl:value-of select="$replacement"/>
<xsl:call-template name="lrReplace3">
<xsl:with-param name="theString" select=
"substring-after($theString, $target)"/>
<xsl:with-param name="target"
select="$target"/>
<xsl:with-param name="replacement"
select="$replacement"/>
</xsl:call-template>
</xsl:when>
<xsl:otherwise>
<xsl:variable name="lStr"
select="string-length($theString)"/>
<xsl:variable name="lTarget"
select="string-length($target)"/>
<xsl:choose>
<xsl:when test="$lStr >= $lTarget">
<xsl:value-of
select="substring($theString,
1,
$lStr -$lTarget+1
)" />
<u>
<xsl:value-of select="$lTarget -1"/>
</u>
</xsl:when>
<xsl:otherwise>
<u>
<xsl:value-of select="$lStr"/>
</u>
</xsl:otherwise>
</xsl:choose>
</xsl:otherwise>
</xsl:choose>
</xsl:template>
</xsl:stylesheet>
|
In this solution the DVC template lrReplace2 acts somewhat
differently than in the pure DVC solution. It stops splitting the
input string much earlier, when the remaining segments are
relatively long. It then applies to them the simple recursive
template lrReplace3. To achieve this, it compares the length
of the input string with the threshold parameter set by the
user. The simple recursive template is written in such a way that
it fulfils the requirements necessary to solve the problem of
splitting the replacements targets, namely it truncates the output
and generates the element u containing the
displacement.
To compare the performance of the two stylesheets we used 3
input files: transl01.xml (156 kB) , transl02.xml
(313 kB) and transl04.xml (626 kB). Each of them contained
one text node with correspondingly: 10 000, 20 000 and
40 000 replacements targets (strings 'AAA'). They are all
included in the attached source.zip file. Since here we
needed an XSLT processor with node-set extension
implemented, we used MSXML 3.0, which we invoked via the
msxsl.exe command line utility. The transformations were
performed, as before, on a 500 MHZ Pentium, 256 MB RAM PC. We
compared the speed of the two algorithms, the last one using 6
different threshold values. The stylesheet based on just the simple
recursive template could not have been included in the comparison,
since even our smallest input file required a recursion depth of
10 000 which, as expected, was enough to cause call-stack
overflow.
Presented below are the results in milliseconds, obtained by
running each corresponding stylesheet 3 times and taking the
average msxsl.exe- reported time:
| Nr of replac. |
DVC |
Two-stage algorithm with different thresholds |
|
5 |
10 |
100 |
2000 |
3000 |
5000 |
| 10000 |
32507 |
35340 |
13581 |
1554 |
750 |
932 |
1377 |
| 20000 |
65311 |
70732 |
26921 |
3209 |
1542 |
1904 |
2832 |
| 40000 |
131284 |
142386 |
54210 |
6491 |
3208 |
3933 |
5721 |
The two-stage algorithm, with the threshold parameter set
to 5 characters, is functionally equivalent to the classic DVC
solution. It is slower, though, because in this specific case, in
addition to splitting the input string with the DVC template, which
proceeds in exactly the same way in both cases, the two-stage
algorithm requires also one call to the simple recursive template
lrReplace3 for each terminal segment. However, it is enough
to set the threshold to 10 to get this algorithm
considerably overtake the pure DVC one. The transformation time
decreases until the threshold reaches approximately
1000-2000 characters, due to reduced DVC overhead . After that, its
further DVC overhead reduction becomes less and less significant,
whereas the growing size of the segments processed by the
second-stage template gains more significance, and the
transformation time begins to grow.
When processing our test strings, the two-stage algorithm with
optimally set threshold (between 1000 and 2000 characters) turned
out to be over 40 times faster than classic DVC solution. This is a
much more impressive result than in both previous examples, so it
is worthwhile to try to interpret it.
It is relatively easy to interpret the results of the first two
examples. In order to do this let us make three observations:
- Both the pure DVC algorithm, as well as the two-stage
algorithm, contains the same first processing, which requires the
same amount of time to complete. Thus, the difference in
performance between those algorithms results exclusively from the
second stage, which in the first algorithm consists in further DVC
processing, while in the two-stage algorithm it is implemented as
simple recursion. Therefore, the difference in results should be
caused by the difference between the pure DVC and the simple
recursion when applied to relatively short segments of the input
file.
- In the first two examples, processing such a short segment
requires approximately the same number of recursive template calls
in both algorithms. To show this let us for the sake of simplicity
assume that the segment's length ls =
2n. In this case the simple recursion would
require ls - 1 template calls, while the DVC
would require: 1 + 2 + ... + 2n-1 =
2n - 1 = ls - 1 calls (splits),
so, in this special case, the number of recursive calls is exactly
the same in both algorithms.
- The DVC template is much more complicated than the simple
recursive one. First, it has to compute the length of the input by
calling the XPath function count or string-length.
Then, it has to use the operator div and the function
floor. And last, but not least, it has to perform two
r
|