尾巴。 ILAsm中的前缀 - 任何使用示例?

ECMA-335,III.2.4规定了tail. 可以在递归函数中使用的前缀。 但是,我无法在C#和F#代码中找到它的用法。 有没有使用的例子?


您不会在当前MS C#编译器生成的任何代码中找到它。 您可以从F#编译器生成的代码中找到它,但由于几乎相反的原因,没有您期望的那么多。

现在,首先要纠正你的陈述中的一个错误:

ECMA-335,III.2.4规定了尾巴。 可以在递归函数中使用的前缀。

这不完全正确。 tail. 前缀可用于被叫呼叫; 并非所有的递归函数都是尾递归,并不是所有的尾调用都是递归的一部分。

尾调用是对函数(包括OOP方法)的任何调用,其中该代码路径中的最后一个操作将进行该调用,然后返回其返回的值,或者只是在调用的函数未返回值时才返回。 因此在:

int DoSomeCalls(int x)
{
  if(A(x))
    return B(x);
  if(DoSomeCalls(x * 2) > 3)
  {
    int ret = C(x);
    return ret;
  }
  return D(DoSomeCalls(x-1));
}

在这里,对BD的调用是尾调用,因为调用之后完成的唯一事情是返回它们返回的值。 到呼叫C不是尾呼叫,但它可以通过去除多余的分配容易地转换为一个ret通过仅直接返回。 对A的调用不是一个尾部调用,并且对DoSomeCalls的调用DoSomeCalls ,但它们是递归的。

现在,正常的函数调用机制是依赖于实现的,但通常涉及在调用堆栈之后保存可能需要的enrigstered值,将参数与当前指令位置一起放入堆栈和/或寄存器中(返回) ,移动指令指针,然后在指令指针移回到完成调用的位置时,从寄存器或堆栈中读取返回值。 使用尾调用可以跳过很多这种情况,因为被调用函数可以使用当前的堆栈帧,然后直接返回到先前的调用者。

tail. 前缀请求,这是通过调用完成的。

虽然这不一定与递归有关,但在说到递归时您是正确的,因为在递归情况下消除尾部调用的好处比其他情况更大; 在实际使用函数调用机制时,调用堆栈空间中的O(n)在堆栈空间中变为O(1),同时减少每个项目的常量时间成本(因此它仍然是O(n)但是O(n)时间意味着需要n×k秒,并且我们有一个更小的k)。 在许多情况下,这可能是工作调用和抛出StackOverflowException的调用之间的区别。

现在,在ECMA-335中有几个关于如何tail.案例tail. 可能并不总是被尊重。 特别是第§3.2.4条的内容规定:

还可以有阻止尾部的特定于实现的限制。 在某些情况下服从的前缀。

最宽松的是,我们可以把它解释为阻止所有情况。

相反,抖动允许应用所有优化方式,包括执行尾部呼叫消除,即使它没有被tail.请求tail.

因此,实际上有四种方法可以在IL中完成尾部消除:

  • 使用tail. 在通话之前添加前缀,并将其授予(不保证)。
  • 不要使用tail. 在通话之前添加前缀,但让抖动决定以任何方式应用(甚至更少保证)。
  • 使用jmp IL指令,它实际上是尾部调用消除的特例(从未被C#使用,因为它为通常相对较小的增益产生了无法验证的代码,尽管由于相对简单,它可能是手动编码时最简单的方法) 。
  • 重写整个方法以使用不同的方法; 特别是可以重写最能从尾调用消除中受益的递归代码,以明确使用这种迭代算法,尾调用消除有效地将递归转换为。*(换句话说,尾调用消除发生之前,甚至编辑)。
  • (有些情况下调用是内联的,因为它不需要一个新的堆栈框架,而且通常整体上通常有更强的改进,并且反过来又经常允许进行更进一步的优化,但是它不是' t通常被认为是尾声消除的情况,因为它是不依赖于尾声呼叫的呼叫消除)。

    现在,在很多情况下,即使请求,抖动的第一个实现也不会执行尾呼叫消除。

    同时在C#方面,有一个决定不发出tail. C#没有大量优化代码生成的一般方法。 有一些优化(特别是去除死代码),但大多数情况下,由于优化工作可能会重复那些由抖动完成的工作(甚至可能妨碍他们),所以优化的缺点(更多并发症意味着更多可能的错误,而且IL对许多开发者来说会更加困惑)相对超过了优势。 使用tail. 就是这样一个典型的例子,因为有时坚持使用tail调用的代价实际上比使用.NET节省更多的代价,所以如果当它是一个好主意的时候抖动已经试图解决,那么C#编译器就会变得更大大部分时间都让事情变得更糟,其余的事情都没有改变。

    同样值得注意的是,使用像C#这样的C风格语言最常见的编码风格:

  • 与其他语言中更常见的样式相比,开发人员往往不会编写特别受益于尾声消除的代码。
  • 开发人员倾向于知道如何优化递归调用,这种调用通过将它们重写为迭代来从尾调用消除中受益。
  • 开发人员首先倾向于以迭代的方式编写它们。
  • 现在,F#一起来了。

    通过F#鼓励的功能和声明性编程,有很多情况下C#中迭代方式最自然地完成的事情最自然地用递归方法完成。 在那些黑客使用C风格语言的人学习将递归案例转化为迭代代码的过程中,黑客使用F#风格的语言学习将迭代案例转化为递归代码,将非尾调用递归代码转化为尾调用递归代码。

    所以F#用tail. 很多。

    而且它得到了很多StackOverflowException ,因为抖动并没有遵守它。

    这是导致抖动人群增加消除尾部呼叫的情况的原因之一,无论是在一般情况下还是在tail. 用来。

    同时,F#人不能只依靠tail. 所以F#的编译器比C#优化得多。 就像我们可以手动地将递归调用重写为脚注中的迭代一样,所以F#编译器在生成IL时会执行相同的操作。

    由于这个原因,很多时候当你编写一个F#方法时,你希望看到一些使用tail. IL tail. ,你实际上得到的是IL反复做同样的事情。

    但是,F#仍然会使用tail. 当一个方法以相互递归的方式调用另一个方法时,如:

    let rec even n = 
      if n = 0 then 
        true 
      else
        odd (n-1)
    and odd n =
      if n = 1 then 
        true 
      else
        even (n-1)
    

    我完全从这个答案中偷了,因为我只用F#玩过一点点,所以我宁愿依靠比我更熟悉的人。

    在这种情况下,由于尾部调用不在单个函数中,因此不能在IL编译点重新编写以消除尾部调用,因此它必须希望抖动能够消除并使用tail. 增加它的机会。


    *将递归调用变为迭代的一个例子是以递归调用开始,如:

    void ClearAllNodes(Node node)
    {
      if(node != null)
      {
        node.Value = null;
        ClearAllNodes(node.Next)
      }
    }
    

    最简单的改变是,然后手动添加尾巴消除功能,通过自己设置参数,然后跳回到方法的开始处:

    void ClearAllNodes(Node node)
    {
    start:
      if(node != null)
      {
        node.Value = null;
        node = node.Next;
        goto start;
      }
    }
    

    既然有充分的理由可以避免goto ,我们通常会将其更改为通过更严格定义的循环机制来做同样的事情:

    void ClearAllNodes(Node node)
    {
      while(node != null)
      {
        node.Value = null;
        node = node.Next;
      }
    }
    
    链接地址: http://www.djcxy.com/p/80621.html

    上一篇: tail. prefix in ILAsm – any example of use?

    下一篇: Stack overflows and recursive sequence expressions F#