【问题标题】:Tail Call Optimization in XSLT/Saxon?XSLT/Saxon 中的尾调用优化?
【发布时间】:2019-10-15 01:05:38
【问题描述】:

谁能解释为什么下面的代码会出现堆栈溢出?我曾希望 Saxon 将模板识别为尾递归,并对其进行优化,允许大量迭代 - 实际上它在大约 1000 次迭代后会出现堆栈溢出。我正在执行如下:

me@server:~/dev$ java -classpath /usr/local/share/java/saxon9ee.jar net.sf.saxon.Transform -it -xsl:recurse.xslt 
437
Exception in thread "main" java.lang.StackOverflowError
        at net.sf.saxon.expr.instruct.ParameterSet.getIndex(ParameterSet.java:127)
        at net.sf.saxon.expr.XPathContextMajor.useLocalParameter(XPathContextMajor.java:561)
        at EE_sequence_02125238280.process(file:/home/me/dev/recurse.xslt:23)
        at com.saxonica.ee.bytecode.CompiledExpression.process(CompiledExpression.java:84)
        at com.saxonica.ee.bytecode.ByteCodeCandidate.process(ByteCodeCandidate.java:143)
        at com.saxonica.ee.bytecode.ByteCodeCandidate.processLeavingTail(ByteCodeCandidate.java:178)
        at net.sf.saxon.expr.instruct.NamedTemplate.expand(NamedTemplate.java:263)
        at EE_sequence_02125238280.process(file:/home/me/dev/recurse.xslt:23)
and so on.....

我正在使用 Saxon-EE 9.8.0.15J。

我尝试过使用<xsl:if>、XPATH 和函数来代替<xsl:choose>,但我遇到了同样的问题。

使用call-templates,我实际上可以在网上找到 cmets 建议这应该可以工作,下面的示例与我的类似。我不是 100% 清楚是否支持 XPATH 表达式中的函数或递归调用,因此我在此示例中坚持使用 call-templates。 例如:Recursive Loop XSLT

我想我错过了一个技巧 - 有什么想法吗?

<?xml version="1.0" encoding="UTF-8"?>                                                                                                                                                                                             
<xsl:stylesheet version="3.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:map="http://www.w3.org/2005/xpath-functions/map"  exclude-result-prefixes="xs map">               

    <xsl:output method="text" version="1.0" encoding="UTF-8" indent="yes"/>                                                                                                                                                        

    <xsl:template name="xsl:initial-template">                                                                                                                                                                                     
        <xsl:variable name="freqs" select="unparsed-text-lines('input.txt', 'UTF-8')!xs:integer(.)"/>                                                                                                                              
        <xsl:message select="sum($freqs)"/>                                                                                                                                                                                        
        <xsl:variable name="hash" select="map{}" as="map(xs:integer, xs:boolean)"/>                                                                                                                                                
        <xsl:call-template name="find-repeated-cs">                                                                                                                                                                                
            <xsl:with-param name="freqs" select="$freqs"/>                                                                                                                                                                         
            <xsl:with-param name="cs-hash" select="$hash"/>                                                                                                                                                                        
        </xsl:call-template>                                                                                                                                                                                                       
    </xsl:template>                                                                                                                                                                                                                

    <xsl:template name="find-repeated-cs">                                                                                                                                                                                         
        <xsl:param name="freqs" as="xs:integer*"/>                                                                                                                                                                                 
        <xsl:param name="cs-hash" as="map(xs:integer, xs:boolean)"/>                                                                                                                                                               
        <xsl:param name="cs" select="0" as="xs:integer"/>                                                                                                                                                                          
        <xsl:param name="i" select="1" as="xs:integer"/>                                                                                                                                                                           
        <xsl:variable name="new-cs" select="$cs + $freqs[$i]" as="xs:integer"/>                                                                                                                                                    
        <xsl:variable name="new-i" select="if ($i >= count($freqs)) then 1 else $i + 1" as="xs:integer"/>                                                                                                                          
        <xsl:choose>                                                                                                                                                                                                               
            <xsl:when test="map:contains($cs-hash, $new-cs)">                                                                                                                                                                      
                <xsl:value-of select="$new-cs"/>                                                                                                                                                                                   
            </xsl:when>                                                                                                                                                                                                            
            <xsl:otherwise>                                                                                                                                                                                                        
                <xsl:call-template name="find-repeated-cs">                                                                                                                                                                        
                    <xsl:with-param name="freqs" select="$freqs"/>                                                                                                                                                                 
                    <xsl:with-param name="cs-hash" select="map:put($cs-hash,$new-cs,true())"/>                                                                                                                                     
                    <xsl:with-param name="cs" select="$new-cs"/>                                                                                                                                                                   
                    <xsl:with-param name="i" select="$new-i"/>                                                                                                                                                                     
                </xsl:call-template>                                                                                                                                                                                               
            </xsl:otherwise>                                                                                                                                                                                                       
        </xsl:choose>                                                                                                                                                                                                              
    </xsl:template>                                                                                                                                                                                                                
</xsl:stylesheet>

编辑

对于一些上下文,代码在累积和序列中找到第二次出现的数字,该数字是通过重复循环一组固定的整数 freqs 而生成的。最新的累积和是cs,过去看到的累积和的字典在cs-hash 中建立。 ifreq 索引为循环索引/计数器。

如果我的方法很愚蠢,我也对其他方法感兴趣,但我仍然想了解为什么无法优化此代码 - 即使有更好的方法。

编辑 2

为了完整起见,使用xsl:choose

<?xml version="1.0" encoding="UTF-8"?>                                                                                                                                                            
<xsl:stylesheet version="3.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:map="http://www.w3.org/2005/xpath-functions/map" xmlns:aoc2018="\
http://www.blah.co.uk/aoc2018" exclude-result-prefixes="xs map aoc2018">                                                                                                                          
    <!-- hint: java -classpath /usr/local/share/java/saxon9ee.jar net.sf.saxon.Transform -it -xsl:01.xslt -->                                                                                     
    <xsl:function name="aoc2018:find-repeated-cs">                                                                                                                                                
        <xsl:param name="freqs" as="xs:integer*"/>                                                                                                                                                
        <xsl:param name="cs-hash" as="map(xs:integer, xs:boolean)"/>                                                                                                                              
        <xsl:param name="cs" as="xs:integer"/>                                                                                                                                                    
        <xsl:param name="i" as="xs:integer"/>                                                                                                                                                     
        <xsl:variable name="new-cs" select="$cs + $freqs[$i]" as="xs:integer"/>                                                                                                                   
        <xsl:choose>                                                                                                                                                                              
            <xsl:when test="map:contains($cs-hash, $new-cs)">                                                                                                                                     
                <xsl:value-of select="$new-cs"/>                                                                                                                                                  
            </xsl:when>                                                                                                                                                                           
            <xsl:otherwise>                                                                                                                                                                       
                <xsl:variable name="new-i" select="if ($i >= count($freqs))                                                                                                                       
                                                   then 1                                                                                                                                         
                                                   else $i + 1" as="xs:integer"/>                                                                                                                 
                <xsl:value-of select="aoc2018:find-repeated-cs($freqs, map:put($cs-hash,$new-cs,true()), $new-cs, $new-i)"/>                                                                      
            </xsl:otherwise>                                                                                                                                                                      
        </xsl:choose>                                                                                                                                                                             
    </xsl:function>                                                                                                                                                                               
    <xsl:output method="text" version="1.0" encoding="UTF-8" indent="yes"/>                                                                                                                       
    <xsl:template name="xsl:initial-template">                                                                                                                                                    
        <xsl:variable name="freqs" select="unparsed-text-lines('input.txt', 'UTF-8')!xs:integer(.)"/>                                                                                             
        <xsl:message select="sum($freqs)"/>                                                                                                                                                       
        <xsl:variable name="hash" select="map{}" as="map(xs:integer, xs:boolean)"/>                                                                                                               
        <xsl:message select="aoc2018:find-repeated-cs($freqs, $hash, 0, 1)"/>                                                                                                                     
    </xsl:template>                                                                                                                                                                               
</xsl:stylesheet>     

以及使用 XPATH 的函数实现:

<?xml version="1.0" encoding="UTF-8"?>                                                                                                                                                            
<xsl:stylesheet version="3.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:map="http://www.w3.org/2005/xpath-functions/map" xmlns:aoc2018="\
http://www.blah.co.uk/aoc2018" exclude-result-prefixes="xs map aoc2018">                                                                                                                          
    <!-- hint: java -classpath /usr/local/share/java/saxon9ee.jar net.sf.saxon.Transform -it -xsl:01.xslt -->                                                                                     
    <xsl:function name="aoc2018:find-repeated-cs">                                                                                                                                                
        <xsl:param name="freqs" as="xs:integer*"/>                                                                                                                                                
        <xsl:param name="cs-hash" as="map(xs:integer, xs:boolean)"/>                                                                                                                              
        <xsl:param name="cs" as="xs:integer"/>                                                                                                                                                    
        <xsl:param name="i" as="xs:integer"/>                                                                                                                                                     
        <xsl:variable name="new-cs" select="$cs + $freqs[$i]" as="xs:integer"/>                                                                                                                   
        <xsl:variable name="new-i" select="if ($i >= count($freqs))                                                                                                                               
                                                   then 1                                                                                                                                         
                                                   else $i + 1" as="xs:integer"/>                                                                                                                 
        <xsl:value-of select="if (map:contains($cs-hash, $new-cs))                                                                                                                                
                              then $new-cs                                                                                                                                                        
                              else aoc2018:find-repeated-cs($freqs, map:put($cs-hash,$new-cs,true()), $new-cs, $new-i)"/>                                                                         
    </xsl:function>                                                                                                                                                                               
    <xsl:output method="text" version="1.0" encoding="UTF-8" indent="yes"/>                                                                                                                       
    <xsl:template name="xsl:initial-template">                                                                                                                                                    
        <xsl:variable name="freqs" select="unparsed-text-lines('input.txt', 'UTF-8')!xs:integer(.)"/>                                                                                             
        <xsl:message select="sum($freqs)"/>                                                                                                                                                       
        <xsl:variable name="hash" select="map{}" as="map(xs:integer, xs:boolean)"/>                                                                                                               
        <xsl:message select="aoc2018:find-repeated-cs($freqs, $hash, 0, 1)"/>                                                                                                                     
    </xsl:template>                                                                                                                                                                               
</xsl:stylesheet>    

【问题讨论】:

  • 当您使用 XSLT 3 时,我想说避免 StackOverflowError 并确保“尾调用优化”的正确尝试是使用 xsl:iterate。我也不太确定你想要实现什么,无论是左折叠还是一些相邻的分组都不是一种更简单的方法。如果您解释代码应该执行哪种计算,将会有所帮助。
  • @MartinHonnen - 谢谢,看看xsl:iterate - 我在底部的原始问题中添加了一些细节,描述了应该发生的事情。代码正确生成累积总和,直到引发堆栈溢出。
  • 但是对于任何输入集,递归是否应该停止?你不是一次又一次地总结价值,即使是像1,2,3这样的简单序列?
  • @MartinHonnen - 它不会为每个输入集停止,但会为某些输入集。这是一个古老的代码出现问题 - 示例输入集可在此处获得 adventofcode.com/2018/day/1/input - 我的各种语言的工作实现可在此处查看 github.com/falloutphil/aoc_2018/tree/master/day_01 - Python 最容易理解,R 最接近上面的 XSLT。 XSLT 实现与工作实现的输出相匹配(直到堆栈溢出)。我知道 XSLT 不是完美的语言,我很享受挑战!
  • github.com/falloutphil/aoc_2018/blob/master/day_01/input.txt 文件似乎可以在 9.8 HE、9.9 HE 和 9.9 EE 中正常工作,所以我想在 9.8 EE 中打开字节码编译似乎是个问题。让我们等待 Saxonica/Michael Kay 是否看到该线程并认为它是一个需要修复的错误。

标签: recursion xslt xpath saxon xslt-3.0


【解决方案1】:

确认这在 9.9 下有效(使用尾递归),无论是否启用字节码生成,但在 9.8 下,只有关闭字节码生成才能成功。我认为版本之间的区别在于 9.9 在决定不使用字节码生成会干扰尾递归的情况下更聪明。

要了解为什么在使用函数调用而不是模板时它会失败,我需要查看代码。这两种情况在内部使用不同的机制。特别是函数默认在“拉”模式下评估(它们返回结果的迭代器),在“推”模式下的模板(它们将结果写入结果树)。最明显的区别是返回包含递归调用结果的序列(例如select="$x, f:myself($x - 1))可以使用模板进行尾递归,但不能使用函数。但这似乎不适用于您的情况。此外,对于模板,我们处理两个或多个模板的相互递归,而对于函数,我们只处理自递归。

以下版本似乎可以使用 9.8 或 9.9 使用尾递归,无论是否生成字节码。 (不过,在 9.8 下,有一个奇怪的地方我还没来得及调查:在产生输出值之后,进程实际上并没有退出。)

<?xml version="1.0" encoding="UTF-8"?>                                                                                                                                                                                             
<xsl:stylesheet version="3.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:xs="http://www.w3.org/2001/XMLSchema" 
    xmlns:map="http://www.w3.org/2005/xpath-functions/map" xmlns:f="f" exclude-result-prefixes="#all">               

    <xsl:output method="text" version="1.0" encoding="UTF-8" indent="yes"/>

    <xsl:param name="limit" select="2000"/>

    <xsl:template name="xsl:initial-template">                                                                                                                                                                                     
        <xsl:variable name="freqs" select="1 to $limit"/>                                                                                                                              
        <xsl:message select="sum($freqs)"/>                                                                                                                                                                                        
        <xsl:variable name="hash" select="map{}" as="map(xs:integer, xs:boolean)"/>   
        <xsl:sequence select="f:find-repeated-cs($freqs, $hash, 0, 1)"/>                                                                                                                                                                                                 
    </xsl:template>                                                                                                                                                                                                                

    <xsl:function name="f:find-repeated-cs">                                                                                                                                                                                         
        <xsl:param name="freqs" as="xs:integer*"/>                                                                                                                                                                                 
        <xsl:param name="cs-hash" as="map(xs:integer, xs:boolean)"/>                                                                                                                                                               
        <xsl:param name="cs" as="xs:integer"/>                                                                                                                                                                          
        <xsl:param name="i" as="xs:integer"/>                                                                                                                                                                           
        <xsl:variable name="new-cs" select="$cs + $freqs[$i]" as="xs:integer"/>                                                                                                                                                    
        <xsl:variable name="new-i" select="if ($i >= count($freqs)) then 1 else $i + 1" as="xs:integer"/>                                                                                                                          
        <xsl:choose>                                                                                                                                                                                                               
            <xsl:when test="map:contains($cs-hash, $new-cs)">                                                                                                                                                                      
                <xsl:value-of select="$new-cs"/>                                                                                                                                                                                   
            </xsl:when>                                                                                                                                                                                                            
            <xsl:otherwise>                                                                                                                                                                                                        
                <xsl:sequence select="f:find-repeated-cs($freqs, map:put($cs-hash,$new-cs,true()), $new-cs, $new-i)"/>                                                                                                                                                                                                                                                                                                                                                                   
            </xsl:otherwise>                                                                                                                                                                                                       
        </xsl:choose>                                                                                                                                                                                                              
    </xsl:function>                                                                                                                                                                                                                
</xsl:stylesheet>

更新

实际上,当我说它有效时,我的意思是它不会因堆栈溢出而失败。进一步检查表明它实际上并没有终止 - 似乎终止条件永远不会为真。我还没有尝试找出原因。

在您的代码中,您使用&lt;xsl:value-of&gt; 来返回函数结果,而不是xsl:sequencexsl:value-of 传递一个文本节点,该节点需要从递归调用的结果构造:您可以在 -explain 输出中看到这一点:

<function name="Q{http://www.blah.co.uk/aoc2018}find-repeated-cs"
              line="5"
              module="file:/Users/mike/Desktop/temp/test.xsl"
              eval="9"
              flags="pU"
              as="item()*"
              slots="6">
      <arg name="Q{}freqs" as="xs:integer*"/>
      <arg name="Q{}cs-hash" as="map(xs:integer, xs:boolean)"/>
      <arg name="Q{}cs" as="xs:integer"/>
      <arg name="Q{}i" as="xs:integer"/>
      <let role="body"
           baseUri="file:/Users/mike/Desktop/temp/test.xsl"
           ns="xsl=~ aoc2018=http://www.blah.co.uk/aoc2018 xs=~ map=~"
           line="10"
           var="Q{}new-cs"
           as="xs:integer"
           slot="4"
           eval="16">
        <check card="1" diag="3|0|XTTE0570|new-cs">
          <arith op="+" calc="i+i">
            <varRef name="Q{}cs" slot="2"/>
            <subscript>
              <varRef name="Q{}freqs" slot="0"/>
              <varRef name="Q{}i" slot="3"/>
            </subscript>
          </arith>
        </check>
        <let line="13" var="Q{}new-i" as="xs:integer" slot="5" eval="16">
          <choose>
            <vc op="ge" onEmpty="0" comp="CAVC">
              <varRef name="Q{}i" slot="3"/>
              <fn name="count">
                <varRef name="Q{}freqs" slot="0"/>
              </fn>
            </vc>
            <int val="1"/>
            <true/>
            <arith op="+" calc="i+i">
              <varRef name="Q{}i" slot="3"/>
              <int val="1"/>
            </arith>
          </choose>
          <valueOf line="16">
            <fn name="string-join">
              <convert from="xs:anyAtomicType" to="xs:string">
                <choose>
                  <ifCall name="Q{http://www.w3.org/2005/xpath-functions/map}contains"
                          type="xs:boolean">
                    <varRef name="Q{}cs-hash" slot="1"/>
                    <varRef name="Q{}new-cs" slot="4"/>
                  </ifCall>
                  <varRef name="Q{}new-cs" slot="4"/>
                  <true/>
                  <data>
                    <mergeAdj>
                      <ufCall name="Q{http://www.blah.co.uk/aoc2018}find-repeated-cs"
                              tailCall="false"
                              bSlot="0"
                              eval="6 16 6 6">
                        <varRef name="Q{}freqs" slot="0"/>
                        <treat as="map(xs:integer, xs:boolean)" diag="0|1||aoc2018:find-repeated-cs">
                          <ifCall name="Q{http://www.w3.org/2005/xpath-functions/map}put" type="map(*)">
                            <varRef name="Q{}cs-hash" slot="1"/>
                            <varRef name="Q{}new-cs" slot="4"/>
                            <true/>
                          </ifCall>
                        </treat>
                        <varRef name="Q{}new-cs" slot="4"/>
                        <varRef name="Q{}new-i" slot="5"/>
                      </ufCall>
                    </mergeAdj>
                  </data>
                </choose>
              </convert>
              <str val=" "/>
            </fn>
          </valueOf>
        </let>
      </let>
    </function>

因为需要对函数结果进行进一步的操作(即转化为文本节点,包括将结果原子化,将结果中的项转化为字符串,并用空格分隔)函数不是tail -递归。始终使用xsl:sequence返回函数结果,并始终声明函数的结果类型和参数的类型。

【讨论】:

  • 感谢@MichaelKay - 我已经使用 function+xsl:choose 和 function+XPATH 添加了问题的代码版本 - 这些似乎都不适用于 9.9?
  • 同样在上面,所以我敢肯定 - 你已经证明,通过提供 1 到 2000 的任意序列(而不是文件),尾递归优化就会启动并且没有发生堆栈溢出?
  • 我很确定用依赖于全局参数的序列替换 unparsed-text-lines() 调用不会影响优化。
  • Confirmed using xsl:sequence 表示 OP 工作中函数的两个版本。谢谢!
  • 最后一点 - 对于命名模板解决方案,仅 9.8EE 需要使用 --generateByteCode:off。更正的功能解决方案在 9.8EE 中工作,无需按照 9.9EE 进行切换。
猜你喜欢
  • 2017-06-09
  • 2012-08-19
  • 2011-05-27
  • 2019-04-20
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2011-03-31
  • 2014-04-06
相关资源
最近更新 更多